You’ve prepared your configuration — now ship
it. Doota deploys as one stack: the three Workers plus everything they
bind (D1, KV, R2, queues, the Durable Object, the email sender) are declared
in infra/alchemy.run.ts, and a single command reconciles your Cloudflare
account against that declaration. No manual resource creation, no ID wiring,
no secret-by-secret pushes — and required secrets you didn’t provide are
minted automatically on the first run.
There are two ways to run that command. Pick one:
| Path | Best for | Auth |
|---|---|---|
| A. From your machine | First deploy, personal instances, test stages | Browser sign-in, no token |
| B. From GitHub | Teams, production — every merge to main deploys itself |
Three repository secrets |
Most people start with A to see their instance come up, then wire up B so production stays current without touching a terminal.
A. Deploy from your machine
1. Sign in to Cloudflare
cd infra
pnpm alchemy loginOpens the browser for OAuth and saves a profile to ~/.alchemy. One-time per
machine — no API token needed for this path.
2. Deploy
From the repository root:
pnpm infra:deployThis builds the web app and deploys the stack. The first run creates
everything: database (with the full schema applied), bucket, KV, queues, and
the three Workers. When it finishes, the printed webUrl is your running
instance:
webUrl: https://doota-dev-yourname.your-account.workers.devTo preview what a deploy would change without touching anything:
pnpm infra:plan3. Apply your configuration
If you filled infra/.env during Configuration,
it was already applied. Changing config later is the same motion: edit
infra/.env, run pnpm infra:deploy again — config re-applies on every
deploy, and only the changes go out.
B. Auto-deploy from GitHub
.github/workflows/deploy.yml deploys the prod stage on every push
to main — a merged PR is a push, so once this is set up, shipping Doota
is just merging. No alchemy login, no .env file in CI; minted secrets
come from the state store.
1. Create the deploy token
In the Cloudflare dashboard: My Profile → API Tokens → Create Custom Token, with:
- Account: Workers Scripts, D1, Workers KV Storage, Workers R2 Storage, Queues, Secrets Store — all Edit; Account Settings Read.
- Zone (each zone in
ORIGINS): Workers Routes Edit, DNS Edit, Zone Read. Skip if you’re workers.dev-only.
This is not the same token as APP_CLOUDFLARE_API_TOKEN —
two tokens, two jobs.
2. Add secrets and variables to the repository
Everything lives in one place: your repo on GitHub → Settings → Secrets and variables → Actions. Two tabs matter:
- Secrets tab → New repository secret — for sensitive values. Write-only once saved; masked in logs.
- Variables tab → New repository variable — for plain config like URLs.
Add these (names must match exactly — the workflow forwards them as-is):
| Name | Where | Required | Value |
|---|---|---|---|
CLOUDFLARE_API_TOKEN |
Secret | ✓ | The deploy token from step 1 |
CLOUDFLARE_ACCOUNT_ID |
Secret | ✓ | Your account id (dashboard → Workers & Pages → right sidebar) |
SETUP_TOKEN |
Secret | ✓ | Any string ≥ 8 chars you choose — you’ll open /setup with it. The deploy fails without it |
APP_CLOUDFLARE_API_TOKEN |
Secret | for mail | The app’s runtime token — without it, domain onboarding is off |
APP_CLOUDFLARE_ACCOUNT_ID |
Secret | for mail | Same account id (the app provisions in it) |
ORIGINS |
Variable | for domains | Comma-separated full URLs, first canonical — e.g. https://mail.acme.com. Unset → workers.dev |
CRON_SECRET, UNSUBSCRIBE_URL, LOG_LEVEL |
Secret / Variable / Variable | — | Optional features |
Nothing else is needed: MAIL_DEK, MAIL_SEARCH_KEY, BETTER_AUTH_SECRET,
and the web-push keypair mint themselves on the first CI deploy and persist
in the stack’s state store — later runs reuse the same values automatically.
3. Merge to main
That’s the whole deployment procedure from here on. The workflow also has a manual trigger (Actions → Deploy → Run workflow) for re-deploys and the one-time adoption run below.
Coexisting with a wrangler deployment
Already running Doota deployed with wrangler deploy? Nothing changes for
it: the stack only ever creates stage-suffixed names, so your bare-name
Workers, database, and bucket are never touched — the two deployments live
side by side. To move an instance’s data onto a stage, do a manual
export/import (D1 export → import, R2 object copy) and provide that
deployment’s live secrets via the deploy env so the imported data stays
decryptable.
Next: first run
Your Workers are live but no mail flows yet — Email Routing and sending come up per domain. Continue to First run to create the super-admin, onboard a domain, make a mailbox, and send a test message.