Skip to content

@storyshelf/core

View as Markdown

@storyshelf/core is the domain layer at the center of StoryShelf. It provides adapter contracts, models, the server-side capture pipeline, pixel diff engine, and retention jobs — with no HTTP dependency, so the capture consumer and forked capture child import it without pulling a server. The HTTP server lives in @storyshelf/app.

Terminal window
npm i @storyshelf/core

JSR JSR Score

Pass a database and storage adapter to createShelfApp from @storyshelf/app (see the app package). Capture, authentication, notifiers (chat, email, git), logging, branding, and server behavior are optional.

import { createShelfApp } from "@storyshelf/app";
const app = createShelfApp({
database, // DatabaseAdapter
storage, // StorageAdapter
testing: { runner: captureRunner, concurrency: 2, compare: { threads: 2 } }, // capture pipeline + screenshot comparison (optional; absent = Storybook hosting only)
auth, // Auth from @storyshelf/auth (optional)
notifications: { notifiers: [...chatNotifiers, ...gitNotifiers] }, // NotifierProvider[]: chat, email and git (optional)
logger, // pino Logger (optional; built internally if omitted)
ui: { name: "My Shelf" }, // UIConfig (optional)
config: {
purgeTtlDays: 30,
branchTtlDays: 30,
branchGcIntervalMs: 86_400_000,
}, // ShelfConfig (optional)
});
// The returned app is a Hono instance; serve it with any Hono adapter.
serve({ fetch: app.fetch, port: 3000 });

Adapter setup hooks kick off in the background when the router is created. Await them before serving for fail-fast startup (or let the first request gate on settlement), and tear down adapters on shutdown:

await app.lifecycle.setup();
const server = serve({ fetch: app.fetch, port: 3000 });
process.on("SIGTERM", () => {
app.lifecycle
.teardown()
.then(() => server.close())
.catch(() => {});
});

The core router is runtime-agnostic (Web Request/Response, fetch, crypto, URL). The Node-specific piece is the capture consumer and its optional forked child, which need a long-lived Node server: that is what the testing option turns on, and the database is the queue (ADR 0006). Without testing an instance only hosts Storybooks and runs on serverless platforms too. You assemble a server for any platform — storyshelf server init generates a scaffold with the adapters you choose:

PlatformDatabaseStoragetesting (capture)Server entry
Vercel, AWS Lambda, Azure Functions@storyshelf/db-sqlite/turso@storyshelf/storage-s3No (hosting only)Hono + platform handler
Cloudflare Workers@storyshelf/db-sqlite/d1 or turso@storyshelf/storage-s3 (R2)No (hosting only)Hono + Workers entry
Bun / Node (VPS, Fly, Railway, Render)db-sqlite (+ ./bun-sqlite on Bun)storage-local / storage-s3Yes, database queuestoryshelf-server serve
Container platforms (ECS, Cloud Run, Container Apps, Kubernetes)db-postgres / db-mysqlstorage-s3 / storage-gcs / storage-azureYes, database queuestoryshelf server init scaffold

All clouds are equal — pick the adapters that match your infrastructure. See the Deployment guide for recipes including a minimal Turso + S3 example.

The barrel (@storyshelf/core) exports only the router and its types (createShelfApp, ShelfOptions, ShelfConfig, UIConfig, ShelfApp). Everything else lives under a subpath:

  • executeCaptureJob({ buildId, reqId }, deps) (core/capture) — the capture orchestrator: loads the build, marks it capturing, extracts the uploaded archive, discovers stories, partitions them into render vs inherit sets (affected capture), delegates rendering to a pure CaptureRunner, and persists. Wired into the Queue when testing is supplied.
  • Review (core/models, Review): BuildReviewModel stores build-level verdicts, the pure deriveReviewStatus and latestVerdicts turn them into a build status, computeChangeset/changesetOf compare a build’s build_components rows with its baseline build’s (components added, removed and changed), and acceptPendingWithoutWork is the pass that accepts a project’s idle pending builds when it turns review off. Retention.purge (core/retention) also removes pending builds idle past the retention window.
  • persistCapture(context) (core/capture) writes screenshots, diffs them against baselines, inherits unaffected stories (inherited: true), and finalizes a build from a renderer’s captures.
  • StorybookAdapter.loadDependencyGraph(sourceDir) (core/capture) — optional StorySourceAdapter hook reading the build’s preview-stats.json for affected tracing; sources without it render everything.
  • Ai, AiError (core/ai) — the AI singleton surface behind ShelfOptions.ai (not an adapter, like Auth); @storyshelf/ai implements it. core/insights holds the pure evidence builders, redaction, result envelopes and budget math. See the AI insights guide.
  • diffImages(baseline, current, options) (core/diff) performs the pixel-level comparison.
  • createDatabaseCaptureQueue(db, options) (core/capture) is the job queue on the app’s own database (capture_jobs), and createCaptureConsumer({ queue, capture, handlers, config }) the consumer that runs its jobs, with a handler per job kind.
  • createUrlBuilder(baseUrl, publishedBaseDomain?) (core/urls) builds application and published Storybook URLs.

The router requires a DatabaseAdapter and StorageAdapter. Auth (from @storyshelf/auth), CaptureRunner, a pino Logger, and notifiers are optional. Auth is a singleton, not an adapter family — one engine implementation, configured in code. All other adapters are constructor-injected, so each deployment can choose its own database, storage, and capture implementation. Logging uses pino (createShelfLogger), with optional transports for hosted observability platforms. Every adapter instance exposes mandatory metadata: { name, version, description, kind, category } (version injected via __PKG_VERSION__ at build; category is the family — database, storage, capture-runner, capture-queue, design, notifier — while kind stays an open string so third-party implementations are never blocked) plus an optional lifecycle object for setup, teardown, and probes (all or nothing — a present lifecycle implements setup, teardown, and health). Adapter-specific extensions live in the same metadata object (notifiers add schema, logo and delivery).

Notifications are one adapter family with two delivery kinds (ADR 0014):

  • NotifierProvider — descriptor registered at startup in ShelfOptions.notifications.notifiers. Carries metadata: { kind, name, version, description, logo?, schema, delivery? } (zod schema validates a channel’s config) and create({config, secret, logger}). delivery is "message" (default: Slack, Teams, email) or "state" (GitHub, GitLab).
  • message adapters expose send({channel, formatted}) and deliver a formatted message once.
  • state adapters expose setState({channel, target, state, url}): idempotent, latest call wins. target is a commit status { kind:"status", sha, context } or the one pull-request comment per build { kind:"comment", sha, markdown } (updated via the <!-- storyshelf:<url> --> marker).

Message channels are sent inline with Promise.allSettled. Build-state delivery is a pure function of the build (statusesForBuild: <prefix>/testing or <prefix>/review, <prefix>/storybook and the pull-request comment) written to every enabled state channel of the project by a durable notify job on the database queue, retried with backoff; a given-up delivery is stored on the channel (lastError, lastErrorAt) and raises sys:notify-failed. Packages that implement NotifierProvider (@storyshelf/notify-chat, @storyshelf/notify-email, @storyshelf/notify-git) are wired in alongside the router.

ShelfConfig supports a session secret, published Storybook base domain, capture concurrency, capture viewports, a capture scratch directory (scratchDir, required when testing is enabled), purge TTL (purgeTtlDays), branch GC TTL (branchTtlDays 30, null=disabled) + interval (branchGcIntervalMs 86_400_000 daily), and an optional adapters snapshot ({ [key]: AdapterMetadata }) auto-populated from each adapter’s metadata for introspection. The secret is also used to encrypt channel secrets (chat webhooks, git tokens) at rest. UIConfig controls the name, logo, and light/dark brand themes. favicon is deprecated and ignored — the app serves its own mark so a StoryShelf tab stays recognisable, while logo (defaulting to that mark) remains overridable.

Choose the default adapters in deployment or see the CLI guide for the packaged server entry point.