Deploy
Masthead is server-rendered and runs on Cloudflare Workers, backed by D1 (database) and R2 (media storage). There is no static build to host elsewhere.
Requirements
- A Cloudflare account on the Workers Paid plan. The bundle is over the 3 MiB Workers free-plan limit.
- Node.js 22.12 or later, pnpm 9 or newer.
packageManagerinpackage.jsonpins the exact version, and pnpm 10+ switches to it. - Tested with emdash 0.38.x. Dependencies are pinned to exact versions; EmDash is in beta and ships weekly, so don’t bump them casually.
Before you deploy
- Create the R2 bucket first:
wrangler r2 bucket create masthead-media.wrangler deploycreates the D1 database and KV namespace automatically, but not R2 — a first deploy that fails on a missing bucket can leave a KV namespace behind with no id recorded, which makes the next deploy fail with “already exists”; if that happens, copy the idwranglerreports intokv_namespacesinwrangler.jsoncby hand. - Set the
EMDASH_SITE_URLenvironment variable to your production URL, both as a Workers Build variable and wherever you runwrangler deployfrom. It’s read at build time (not runtime) for three things: Astro’s image optimizer only resizes images from known origins, EmDash’ssiteUrl(unresizable images otherwise get served full-size), and member sign-in link generation. If you’re also using passkeys, this is the same value as the passkeyrpId— it must match the domain you actually deploy to. - If you’re shipping the podcast (Pro), set a
coverimage on thePODCASTconstant insrc/utils/podcast.ts— see customization.md.
Build and deploy
pnpm install
pnpm deploy # astro build && wrangler deploy
Finish setup immediately
A freshly deployed site has a public, unauthenticated /_emdash/api/setup until the first admin account is created — the lock is “does a user row exist,” not a flag. Open the site right after the first deploy and complete the setup wizard (site info, admin account, passkey) before telling anyone the URL.
Then create the sample forms (Pro). The seed embeds a newsletter form in the “Newsletter signup” section and a “Report an error” form on the corrections page, but a seed file cannot hold plugin data, so until you run this those embeds render nothing:
npx wrangler d1 execute DB --remote --file seed/forms.sql
Use --local instead of --remote for pnpm dev. Existing forms are left alone. Pages already cached at the edge show the form once their cache entry refreshes (up to an hour), or right away after any edit in the admin. To send yourself a copy of each submission, add your address under the form’s notification settings in Admin > Forms.
Email (member sign-in, Pro)
Member sign-up and sign-in send a one-time link by email through Cloudflare Email Sending: src/pages/members/index.astro calls sendEmail() in src/plugins/cloudflare-email.ts, which uses the EMAIL binding directly. EmDash’s own mail (admin sign-in, form notifications) goes through the same file registered as an EmDash plugin, for production builds only — having two providers in astro dev stops EmDash from picking one.
In pnpm dev nothing is sent. The member sign-in email is written to a text file under .wrangler/tmp/email/, and the dev log prints its path in a send_email binding called line; open that file for the link (it is removed when the dev server stops). EmDash’s own mail in dev is listed at /_emdash/api/dev/emails.
- Onboard your sending domain once:
npx wrangler email sending enable example.com. - Set
EMAIL_FROMandEMAIL_FROM_NAMEinwrangler.jsoncundervars. - Without this, production sign-in and sign-up fail with
EMAIL_NOT_CONFIGURED(passkey sign-in for existing accounts still works).
Rate limiting
Sign-in requests are capped per IP and per destination address (5 per 60 seconds) via the SIGN_IN_LIMIT binding in wrangler.jsonc. No setup needed — Workers rate limiting bindings don’t require creating a separate resource.
Secrets
EMDASH_ENCRYPTION_KEY (in your local .env; create it with npx emdash secrets generate --write .env) must be set as a Workers secret in production: wrangler secret put EMDASH_ENCRYPTION_KEY. Never commit it.
CI
.github/workflows/ci.yml runs on push to main: typecheck, check:contrast, lint, seed a local database, build, pnpm preview, Lighthouse (LHCI), and an internal link check. Use it as a reference for what to verify before shipping a change.
Next: Customization