Cloud assembly
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.
Mix-and-match layers
Section titled “Mix-and-match layers”| Platform | Database | Storage | Testing | Auth | Server entry |
|---|---|---|---|---|---|
| Vercel | @storyshelf/db-sqlite/turso (Turso/libSQL) | @storyshelf/storage-s3 (R2/S3) | Hosting only (no testing) | @storyshelf/auth | Hono + @hono/vercel-edge |
| Cloudflare Workers | @storyshelf/db-sqlite/turso | @storyshelf/storage-s3 (R2) | Hosting only (no testing) | @storyshelf/auth | Hono + Workers entry |
| Azure Functions | @storyshelf/db-sqlite/turso | @storyshelf/storage-s3 (Azure Blob) | Hosting only (no testing) | @storyshelf/auth | Hono + Azure Functions handler |
| AWS Lambda | @storyshelf/db-sqlite/turso | @storyshelf/storage-s3 (S3) | Hosting only (no testing) | @storyshelf/auth | Hono + Lambda handler |
| Deno Deploy | @storyshelf/db-sqlite/turso | @storyshelf/storage-s3 (R2/S3) | Hosting only (no testing) | @storyshelf/auth | Hono + Deno entry |
| Bun | @storyshelf/db-sqlite or @storyshelf/db-sqlite/bun-sqlite | storage-local or storage-s3 | testing (long-running host) | @storyshelf/auth | Hono + Bun.serve |
Serverless: Storybook hosting only
Section titled “Serverless: Storybook hosting only”Enabling
testingrequires 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 leavingtestingunset.
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.
createShelfAppthrows at boot whenAWS_LAMBDA_FUNCTION_NAMEis set andtesting.runneris configured. - Vercel and Netlify. Boot logs a warning when
testing.runneris configured (VERCELorNETLIFYis set). - Override.
STORYSHELF_ALLOW_SERVERLESS=1silences both, for a host that looks serverless but keeps a process alive. - Cloudflare D1. The
@storyshelf/db-sqlite/d1preset is a Workers-only binding, so it cannot back an instance withtesting.runner;createShelfAppthrows at boot. Turso over the network can. - Uploads. With no consumer on the host, a zip over
maxInlineUnzipSizeis refused with413, andstoryshelf uploadfalls 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.
Postgres providers
Section titled “Postgres providers”@storyshelf/db-postgres uses postgres.js + Drizzle. Use createPostgresDatabase({ url }) for all providers — only the connection string and pooling change.
| Provider | Connection shape | TLS | Pooling |
|---|---|---|---|
| Self-hosted | postgres://user:pass@localhost:5432/shelf | ssl: false | max: 10 |
| AWS RDS | postgres://user:pass@<rds-endpoint>:5432/db?sslmode=require | ssl: true | prepare: true |
| GCP Cloud SQL | postgres://user:pass@<cloudsql-ip>:5432/db | ssl: { ca, cert, key } | Connector or injected client |
| Supabase | postgres://postgres.<ref>:<pass>@<pooler-host>:6543/postgres?pgbouncer=true | ssl: true | prepare: false on :6543 |
| Neon | postgres://user:pass@<neon-host>/db?sslmode=require | ssl: true | max: 5; prepare: false on pooled |
| Azure DB for PG | postgres://user:pass@<server>.postgres.database.azure.com:5432/db?sslmode=require | ssl: true | Entra ID via injected client |
See the @storyshelf/db-postgres package reference for full options.
Related
Section titled “Related”- AWS · Azure · GCP — reference Terraform stacks
- Docker Compose — single-host deploy