@storyshelf/core
@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.
Install
Section titled “Install”npm i @storyshelf/corepnpm add @storyshelf/coreyarn add @storyshelf/core- npm package — install tarballs and version history.
- JSR package — TypeScript-first registry page.
- Public API reference — generated docs for every export; start here to learn the API. (The same reference is on JSR.)
- Source on GitHub — package directory on
main.
Compose a server
Section titled “Compose a server”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(() => {});});Deployment targets
Section titled “Deployment targets”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:
| Platform | Database | Storage | testing (capture) | Server entry |
|---|---|---|---|---|
| Vercel, AWS Lambda, Azure Functions | @storyshelf/db-sqlite/turso | @storyshelf/storage-s3 | No (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-s3 | Yes, database queue | storyshelf-server serve |
| Container platforms (ECS, Cloud Run, Container Apps, Kubernetes) | db-postgres / db-mysql | storage-s3 / storage-gcs / storage-azure | Yes, database queue | storyshelf 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.
Main APIs
Section titled “Main APIs”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 itcapturing, extracts the uploaded archive, discovers stories, partitions them into render vs inherit sets (affected capture), delegates rendering to a pureCaptureRunner, and persists. Wired into theQueuewhentestingis supplied.- Review (
core/models, Review):BuildReviewModelstores build-level verdicts, the purederiveReviewStatusandlatestVerdictsturn them into a build status,computeChangeset/changesetOfcompare a build’sbuild_componentsrows with its baseline build’s (components added, removed and changed), andacceptPendingWithoutWorkis 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) — optionalStorySourceAdapterhook reading the build’spreview-stats.jsonfor affected tracing; sources without it render everything.Ai,AiError(core/ai) — the AI singleton surface behindShelfOptions.ai(not an adapter, likeAuth);@storyshelf/aiimplements it.core/insightsholds 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), andcreateCaptureConsumer({ 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.
Adapter contracts
Section titled “Adapter contracts”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 inShelfOptions.notifications.notifiers. Carriesmetadata: { kind, name, version, description, logo?, schema, delivery? }(zodschemavalidates a channel’s config) andcreate({config, secret, logger}).deliveryis"message"(default: Slack, Teams, email) or"state"(GitHub, GitLab).messageadapters exposesend({channel, formatted})and deliver a formatted message once.stateadapters exposesetState({channel, target, state, url}): idempotent, latest call wins.targetis 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.