Masthead Pro demo. Pro adds the podcast, members-only stories, forms, and page layouts (Cover, Sign-up). Docs · Try the editor, no sign-up → Get Masthead Pro →

Documentation

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. packageManager in package.json pins 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

  1. Create the R2 bucket first: wrangler r2 bucket create masthead-media. wrangler deploy creates 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 id wrangler reports into kv_namespaces in wrangler.jsonc by hand.
  2. Set the EMDASH_SITE_URL environment variable to your production URL, both as a Workers Build variable and wherever you run wrangler deploy from. It’s read at build time (not runtime) for three things: Astro’s image optimizer only resizes images from known origins, EmDash’s siteUrl (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 passkey rpId — it must match the domain you actually deploy to.
  3. If you’re shipping the podcast (Pro), set a cover image on the PODCAST constant in src/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.

  1. Onboard your sending domain once: npx wrangler email sending enable example.com.
  2. Set EMAIL_FROM and EMAIL_FROM_NAME in wrangler.jsonc under vars.
  3. 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