Skip to content
All posts

How Emberstack deploys, from git push to live in about a minute

Push to main and Cloudflare builds and deploys the Worker. Here's the order that keeps database changes safe, how domains and redirects work, and the restore point I take before anything risky.

Roger
A rocket's orange exhaust trail curving up into a dark sky

Emberstack's repo is connected to Cloudflare's GitHub builds. Push to main, and about 60 to 75 seconds later the new version is serving at emberstack.dev.

I found that out the fun way, by deploying manually and then noticing a second deploy from my own account 3 minutes later that I hadn't run. That was GitHub.

So the deploy step is git push. There's still a manual path, and an order of operations that matters more than the button you press.

The manual path

bun run deploy

That builds with PUBLIC_URL=https://emberstack.dev and runs wrangler deploy. Better Auth and the Stripe redirects build their URLs from PUBLIC_URL, so it has to be your real origin. Before I set it, Stripe happily sent people back to localhost:3000 after paying. Ask me how I know.

bunx wrangler deploy --dry-run shows every binding and variable without deploying, which I run whenever I've touched wrangler.jsonc.

Migrations go first

This is the one rule worth tattooing somewhere.

If a deploy reads a new column, the column has to exist before the code arrives. So:

  1. Generate and read the migration: bun run db:generate.
  2. Apply it to production: bun run db:migrate.
  3. Then push or deploy the code that uses it.

Keep each migration additive where you can, so the old code keeps working against the new schema for the minute the two overlap. When the AI assistant shipped, migration 0001 only created 2 tables. Nothing existing changed, so applying it early was safe.

EmDash handles its own database: it applies its core migrations on the first request after an upgrade. Before deploying an EmDash upgrade I still check:

bunx emdash migrate --status --d1 my-app-cms --wrangler-config wrangler.jsonc

Take a restore point first

Before a migration or a bulk content change, I grab a D1 Time Travel bookmark:

bunx wrangler d1 time-travel info my-app-db

It prints the bookmark and the exact restore command. It takes 2 seconds.

One gotcha: wrangler d1 export fails on the CMS database because EmDash uses full-text search tables, so Time Travel is the backup there.

Domains and the old one

Domains are Workers Custom Domains in wrangler.jsonc. Wrangler creates the DNS record and certificate for any zone on the same account.

When Emberstack moved from saasflare.xyz to emberstack.dev, I kept the old domain attached and redirected it in src/worker.ts, which runs before Astro:

const REDIRECT_HOSTS = new Set(['saasflare.xyz'])

fetch(request, env, ctx) {
  const url = new URL(request.url)
  if (REDIRECT_HOSTS.has(url.hostname)) {
    return Response.redirect(`https://emberstack.dev${url.pathname}${url.search}`, 301)
  }
  return handler.fetch(request, env, ctx)
}

Path and query string carry over, so old links land on the right page. Static files are served before the Worker runs, so old image URLs keep working without a redirect at all.

One thing that won't follow a redirect: Stripe webhooks. Point those at the primary domain directly.

Secrets live on the Worker

bunx wrangler secret put BETTER_AUTH_SECRET

Locally they go in .dev.vars, which is gitignored. Everything that isn't secret (PUBLIC_URL, EMAIL_FROM) sits in vars in wrangler.jsonc, in git, where you can see it.

After it's live

bunx wrangler tail streams production logs. And if a deploy goes wrong, Cloudflare keeps previous versions, so rolling back is a command away rather than a rebuild.

The Going to production doc has the first-deploy checklist, including the CMS and Stripe steps.

Continue reading

All posts