# @storyshelf/core

> StoryShelf's domain layer — adapters, models, capture pipeline, and review engine (no HTTP).

Source: https://storyshelf.js.org/packages/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](../app/).

## Install

```sh
npm i @storyshelf/core
```

[![JSR](https://jsr.io/badges/@storyshelf/core)](https://jsr.io/@storyshelf/core) [![JSR Score](https://jsr.io/badges/@storyshelf/core/score)](https://jsr.io/@storyshelf/core)

- [npm package](https://www.npmjs.com/package/@storyshelf/core) — install tarballs and version history.
- [JSR package](https://jsr.io/@storyshelf/core) — TypeScript-first registry page.
- [Public API reference](https://storyshelf.js.org/reference/core/) — generated docs for every export; start here to learn the API. (The same reference is on [JSR](https://jsr.io/@storyshelf/core/doc).)
- [Source on GitHub](https://github.com/GuptaSiddhant/storyshelf/tree/main/packages/core) — package directory on `main`.

## Compose a server

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

```ts
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:

```ts
await app.lifecycle.setup();

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

## 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

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](https://storyshelf.js.org/concepts/affected-capture/)), delegates rendering to a pure `CaptureRunner`, and persists. Wired into the `Queue` when `testing` is supplied.
- Review (`core/models`, [Review](https://storyshelf.js.org/guides/review/#review-without-screenshots)): `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](https://storyshelf.js.org/ai/).
- `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.

## 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 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`](../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](../../guides/deployment/) or see the [CLI guide](../../guides/cli/) for the packaged server entry point.
