Skip to content

Cloud assembly

View as Markdown

StoryShelf is cross-runtime: the core router (createShelfApp) uses only Web-standard APIs (Request, Response, fetch, ReadableStream, crypto, URL). There is no Node coupling in the core, so you can deploy on any platform that runs JavaScript.

PlatformDatabaseStorageTestingAuthServer entry
Vercel@storyshelf/db-sqlite/turso (Turso/libSQL)@storyshelf/storage-s3 (R2/S3)Hosting only (no testing)@storyshelf/authHono + @hono/vercel-edge
Cloudflare Workers@storyshelf/db-sqlite/turso@storyshelf/storage-s3 (R2)Hosting only (no testing)@storyshelf/authHono + Workers entry
Azure Functions@storyshelf/db-sqlite/turso@storyshelf/storage-s3 (Azure Blob)Hosting only (no testing)@storyshelf/authHono + Azure Functions handler
AWS Lambda@storyshelf/db-sqlite/turso@storyshelf/storage-s3 (S3)Hosting only (no testing)@storyshelf/authHono + Lambda handler
Deno Deploy@storyshelf/db-sqlite/turso@storyshelf/storage-s3 (R2/S3)Hosting only (no testing)@storyshelf/authHono + Deno entry
Bun@storyshelf/db-sqlite or @storyshelf/db-sqlite/bun-sqlitestorage-local or storage-s3testing (long-running host)@storyshelf/authHono + Bun.serve

Enabling testing requires a long-running server (a container, VM, Fly Machine or Kubernetes pod). It cannot run on serverless platforms that freeze or end the process after each request (Vercel Functions, AWS Lambda, Cloudflare Workers), because capture jobs run for minutes in the background and the job consumer must keep running. A serverless deployment can host Storybooks only, by leaving testing unset.

testing needs a process that keeps running between requests: capture jobs run for minutes in the background. Vercel Functions, AWS Lambda, Netlify Functions and Cloudflare Workers freeze or end the process after each request, so on those platforms StoryShelf hosts Storybooks only: leave the testing option out. See Hosting only.

  • AWS Lambda. createShelfApp throws at boot when AWS_LAMBDA_FUNCTION_NAME is set and testing.runner is configured.
  • Vercel and Netlify. Boot logs a warning when testing.runner is configured (VERCEL or NETLIFY is set).
  • Override. STORYSHELF_ALLOW_SERVERLESS=1 silences both, for a host that looks serverless but keeps a process alive.
  • Cloudflare D1. The @storyshelf/db-sqlite/d1 preset is a Workers-only binding, so it cannot back an instance with testing.runner; createShelfApp throws at boot. Turso over the network can.
  • Uploads. With no consumer on the host, a zip over maxInlineUnzipSize is refused with 413, and storyshelf upload falls back to the file-by-file route by itself.

Minimal Turso + S3 recipe (long-running host with testing)

Section titled “Minimal Turso + S3 recipe (long-running host with testing)”
import { createShelfApp } from "@storyshelf/app";
import { createTursoDatabase } from "@storyshelf/db-sqlite/turso";
import { createS3Storage } from "@storyshelf/storage-s3";
import { createPlaywrightCaptureRunner } from "@storyshelf/runner-playwright";
const database = createTursoDatabase({
url: process.env.TURSO_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
const storage = createS3Storage({
bucket: process.env.S3_BUCKET!,
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT,
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
});
const capture = createPlaywrightCaptureRunner();
export const app = createShelfApp({
database, storage, testing: { runner: capture, concurrency: 2 },
config: { scratchDir: "/tmp/scratch" },
auth: ...,
});
await app.lifecycle.setup();

Without the testing option the same assembly runs on any platform that supports Node-compatible fetch + crypto + sqlite/libsql (Storybook hosting only). With testing it needs a long-running host.

@storyshelf/db-postgres uses postgres.js + Drizzle. Use createPostgresDatabase({ url }) for all providers — only the connection string and pooling change.

ProviderConnection shapeTLSPooling
Self-hostedpostgres://user:pass@localhost:5432/shelfssl: falsemax: 10
AWS RDSpostgres://user:pass@<rds-endpoint>:5432/db?sslmode=requiressl: trueprepare: true
GCP Cloud SQLpostgres://user:pass@<cloudsql-ip>:5432/dbssl: { ca, cert, key }Connector or injected client
Supabasepostgres://postgres.<ref>:<pass>@<pooler-host>:6543/postgres?pgbouncer=truessl: trueprepare: false on :6543
Neonpostgres://user:pass@<neon-host>/db?sslmode=requiressl: truemax: 5; prepare: false on pooled
Azure DB for PGpostgres://user:pass@<server>.postgres.database.azure.com:5432/db?sslmode=requiressl: trueEntra ID via injected client

See the @storyshelf/db-postgres package reference for full options.