Skip to content
Knowledge sections

Cloudflare deployment

Deploy the starter to Cloudflare Workers, D1, Queues, and Email Service using Alchemy.

cloudflarealchemydeployment
On this page

The starter is Cloudflare-first. Production resources are declared once in the root alchemy.run.ts using Alchemy v2 and provisioned with a single command.

What gets provisioned

  • One D1 database shared by apps/web, apps/api, and apps/background
  • One Worker per app (web, api, background), each with placement.mode = smart where applicable
  • A Cloudflare Queue plus dead-letter queue (b2b-saas-starter-webhooks + b2b-saas-starter-webhooks-dlq) bound as a QueueConsumer on the background worker
  • Cloudflare SendEmail binding with allowedSenderAddresses for outbound transactional mail (env-gated)
  • Cloudflare RateLimit bindings on the web worker (RATE_LIMITER_AUTH_READ, RATE_LIMITER_AUTH_WRITE, RATE_LIMITER_AUTH_SIGN_IN) and on the api worker (RATE_LIMITER_REST, RATE_LIMITER_REST_WRITE, RATE_LIMITER_INVITATIONS, RATE_LIMITER_ASSISTANT, RATE_LIMITER_MCP)

Deploy

bun run deploy

bun run deploy is bun alchemy.run.ts: it executes the Alchemy Stack programmatically rather than going through a separate CLI. Each Worker also has a hand-maintained wrangler.jsonc that mirrors the Alchemy declaration so wrangler dev and wrangler d1 migrations apply keep working locally; Alchemy is the source of truth for production.

Environment configuration

Sensitive values are wrapped in effect/Redacted inside alchemy.run.ts so they never appear in logs or stack traces. Optional providers are validated in packages/env/src/server.ts: missing config keeps the relevant provider inactive rather than failing startup. See the secret matrix in ARCHITECTURE.md for the full list (BETTER_AUTH_SECRET, BETTER_AUTH_URL, BETTER_AUTH_TRUSTED_ORIGINS, STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET, SENTRY_DSN, POSTHOG_KEY/POSTHOG_HOST, CLOUDFLARE_EMAIL_FROM, TURNSTILE_SITE_KEY/TURNSTILE_SECRET_KEY, WORKERS_AI_ENABLED, OPENAI_API_KEY).

Preview deployments per pull request

Every pull request opened from this repository gets its own Alchemy stage, pr-<number>, with a separate D1 database, webhook queues, and three Workers named b2b-saas-starter-pr-<number>-{web,api,background}. The Preview workflow deploys the stage on every push, runs the migrations, seeds the Seed Workspace so the preview opens with data, and keeps one comment on the pull request pointing at the web, API, and background URLs. Closing the pull request destroys the stage.

Previews run provider-light: Turnstile, Stripe, Sentry, PostHog, email, and AI stay off regardless of what the deploying environment has configured, and ENVIRONMENT is preview. The same stage can be deployed from a laptop:

export CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… CLOUDFLARE_WORKERS_SUBDOMAIN=<your subdomain>
export BETTER_AUTH_SECRET="$(openssl rand -base64 32)"
ALCHEMY_STAGE=pr-42 pnpm run deploy:stage
ALCHEMY_STAGE=pr-42 pnpm run db:seed:stage
ALCHEMY_STAGE=pr-42 pnpm run destroy:stage

The required secrets and the reasoning are in docs/deploying.md and ADR 0065.

Rolling back

bun run destroy (which is bun alchemy.run.ts --destroy) tears the entire stack down. Targeted rollbacks today go through redeploying a previous commit from CI; there is no versioned rollback flag in the starter's scripts.