#Deploy Your Own
StoryLark is built to be deployed once per brand from the same codebase: one Cloudflare Worker, one D1 database, one R2 bucket per brand. This guide stands up a new branded site end to end.
Everything below uses placeholders —
<your-...>,example.com,00000000-.... Never commit real account IDs, database IDs, domains, or secrets. The base brand'swrangler.jsoncenv ships with placeholder IDs on purpose; fill in your own only in your deployment, and keep secrets out of the repo (wrangler secret put, below).
#1. Create your brand
Copy the base brand and give it a new id:
cp -r brands/storylark brands/<your-id>
Edit brands/<your-id>/brand.json (identity, origins, nouns, TTS) and
brands/<your-id>/theme.css (colors + fonts). The full field-by-field reference
is in build-your-own-theme.md. At minimum, set:
id— must match the folder name and the--modeyou build with.appName,name,shortName,tagline,author.appOrigin— where the app is served (e.g.https://app.example.com).contentOrigin— where R2 content is served (e.g.https://content.example.com).themeColor/backgroundColor— must match your theme's paper color.layoutandnouns— seebuild-your-own-presentation.md.
#Icons
The manifest references three PNGs under brands/<your-id>/assets/icons/:
| File | Size | Purpose |
|---|---|---|
icon-192.png |
192×192 | Standard |
icon-512.png |
512×512 | Standard |
icon-maskable-512.png |
512×512 | Maskable (safe-zone padded) |
Drop in your own artwork, or generate neutral placeholder icons in your brand colors:
node tools/gen-icons.mjs --brand <your-id>
(The base brand also carries favicon.svg, favicon-32.png, favicon-180.png,
and a logo.svg — supply your own equivalents if your HTML references them.)
#2. Add a Wrangler env for the brand
wrangler.jsonc defines one env per brand, selected with --env. Copy the
storylark block, rename it to <your-id>, and fill in your own resources:
"env": {
"<your-id>": {
"name": "<your-id>",
"routes": [{ "pattern": "app.example.com", "custom_domain": true }],
"d1_databases": [
{
"binding": "DB",
"database_name": "<your-id>",
"database_id": "<your-d1-database-id>",
"migrations_dir": "worker/migrations"
}
],
"r2_buckets": [
{ "binding": "CONTENT", "bucket_name": "<your-id>-content" }
],
"vars": {
"BRAND": "<your-id>",
"APP_ORIGIN": "https://app.example.com",
"CONTENT_ORIGIN": "https://content.example.com",
"MAIL_FROM": "Your App <[email protected]>",
"APP_NAME": "Your App"
}
}
}
The Worker serves app/dist as static assets and runs code only for /api/*
(run_worker_first: ["/api/*"]), with SPA fallback for everything else.
#3. Provision Cloudflare resources
Authenticate once (npx wrangler login), then create the per-brand resources.
D1 database — create it, copy the returned database_id into your env's
database_id, then apply the migrations in worker/migrations/:
npx wrangler d1 create <your-id>
npx wrangler d1 migrations apply <your-id> --env <your-id> --remote
R2 bucket — the content bucket is named <your-id>-content (the publish
pipeline derives this name from the brand id):
npx wrangler r2 bucket create <your-id>-content
Attach an R2 custom domain so the bucket serves at your contentOrigin. The
pipeline uploads objects at the bucket root, and an R2 custom domain serves the
bucket root at the domain root — so content.example.com/manifest.json maps to
the manifest.json object. (See architecture.md for why
content bypasses the Worker.)
#4. Set secrets
These are Worker secrets, never committed. Set the ones you need per env:
npx wrangler secret put VAPID_PUBLIC_KEY --env <your-id>
npx wrangler secret put VAPID_PRIVATE_KEY --env <your-id>
npx wrangler secret put ADMIN_KEY --env <your-id>
npx wrangler secret put RESEND_API_KEY --env <your-id>
npx wrangler secret put GOOGLE_CLIENT_ID --env <your-id>
npx wrangler secret put GOOGLE_CLIENT_SECRET --env <your-id>
| Secret | Needed for | Notes |
|---|---|---|
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY |
Web push | Generate with node tools/gen-vapid.mjs. Put the public key in brand.json vapidPublicKey and the Worker secret; the private key is Worker-only. See push.md. |
ADMIN_KEY |
POST /api/admin/publish |
The publish pipeline sends this as X-Admin-Key to fire push notifications. Without it, publishing still works — it just skips the notify step. |
RESEND_API_KEY |
Magic-link email | Only if you enable the (currently dormant) magic-link path. See auth.md. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google sign-in | Only if you enable the (currently dormant) Google path. |
Password + passkey sign-in need no secrets.
#5. Build and deploy
npm run build -w app -- --mode <your-id>
npx wrangler deploy --env <your-id>
The root npm run deploy is hardcoded to the storylark brand/env; for your own
brand, run the two commands above (or add a matching script to package.json).
#6. Publish content
Your site boots as an empty shelf until you publish. Point the pipeline at your content source and a parser you own:
node tools/publish.mjs --brand <your-id> \
--source <path-to-your-content> \
--parser <path-to-your-parser.mjs>
Full pipeline reference, flags, and the parser contract:
content-pipeline.md.
#Free-tier note
The default architecture is tuned to fit Cloudflare + Azure Speech free tiers
(see the budget table in architecture.md). The publish
pipeline enforces a monthly TTS character budget with a hard stop; heavier usage
means moving off the free tiers.
Found a gap? StoryLark is open source — improve these docs on GitHub.