Deploying to Cloudflare
End-to-end deploy of the Amy backend to Cloudflare. ~30 minutes first time, ~2 minutes for subsequent updates. Everything here is idempotent, re-running a step is safe.
What you'll provision
| Resource | Purpose | Cost at beta scale |
|---|---|---|
| Cloudflare Worker | The API + webhook receiver + cron jobs | $5/month (Workers Paid) |
| D1 database | Relational store (users, turns, biomarkers, summaries) | Free (under 5 GB) |
| R2 bucket | Lab PDFs and future exports | Free (under 10 GB) |
| KV namespace | Idempotency keys + SSE stream buffer | Free (under 100k reads/day) |
| Queue | Async fan-out from Terra webhooks | Included with Workers Paid |
| Workflows | Durable agent run pipeline | Included with Workers Paid |
| Cron triggers | Drain unprocessed events + nightly reconcile | Included |
Required paid tier: Workers Paid plan ($5/mo). Free plan won't have Queues or Workflows.
External accounts: Clerk (auth, free tier OK), Terra (wearables + lab OCR, talk to them for a key), and a Claude model provider (Anthropic API, OpenRouter, or your Claude.ai subscription).
Prerequisites
# 1. Bun (the runtime)
curl -fsSL https://bun.sh/install | bash
# 2. Wrangler (Cloudflare CLI)
bun add -g wrangler
# 3. Sign in to Cloudflare
wrangler login # opens a browser
# 4. Confirm
wrangler whoamiOne-time provisioning
These create the Cloudflare resources. Run each command once; note the
IDs that come back (you'll paste them into wrangler.toml).
Create D1 database
wrangler d1 create amy-dbOutput looks like:
✅ Successfully created DB 'amy-db'
[[d1_databases]]
binding = "DB"
database_name = "amy-db"
database_id = "f2c1dc51-…"Copy the database_id into cloud/wrangler.toml under
[[d1_databases]].
Create R2 bucket
wrangler r2 bucket create amy-lab-reports(No ID to copy; the bucket name is the binding.)
Create KV namespace
wrangler kv namespace create CACHEOutput gives an id; paste into wrangler.toml under [[kv_namespaces]].
Create Queue
wrangler queues create terra-events
wrangler queues create terra-events-dlq # dead-letter queueApply database migrations
cd cloud
bunx wrangler d1 migrations apply amy-db --remoteSet secrets
Secrets are encrypted environment variables, set once per environment.
Run each from inside cloud/:
# Auth (Clerk)
echo "<your-clerk-secret>" | wrangler secret put CLERK_SECRET_KEY
echo "<your-clerk-publishable>" | wrangler secret put CLERK_PUBLISHABLE_KEY
# Terra (wearables + labs)
echo "<terra-api-key>" | wrangler secret put TERRA_API_KEY
echo "<terra-dev-id>" | wrangler secret put TERRA_DEV_ID
echo "<terra-webhook-secret>" | wrangler secret put TERRA_WEBHOOK_SECRET
# Anthropic (or OpenRouter — set one)
echo "<sk-ant-…>" | wrangler secret put ANTHROPIC_API_KEY
# OR
echo "<openrouter-key>" | wrangler secret put OPENROUTER_API_KEY
# Internal
echo "$(openssl rand -hex 32)" | wrangler secret put AMY_JWT_SECRET
echo "$(openssl rand -hex 32)" | wrangler secret put AMY_ADMIN_KEYVerify:
wrangler secret listDeploy
cd cloud
bunx wrangler deployOutput ends with something like:
Published amy-api (1.23 sec)
https://amy-api.YOUR-SUBDOMAIN.workers.devThat URL is your base URL.
Smoke test
export AMY_BASE_URL="https://amy-api.YOUR-SUBDOMAIN.workers.dev"
# Liveness
curl -s "$AMY_BASE_URL/healthz"
# → ok
# OpenAPI spec is being served
curl -s "$AMY_BASE_URL/openapi.json" | jq .info.title
# → "Amy"
# llms.txt is being served
curl -s "$AMY_BASE_URL/llms.txt" | head -5Wire up Terra webhooks
Terra needs to know where to send events.
- Go to the Terra dashboard → Webhooks.
- Add a webhook URL:
https://amy-api.YOUR-SUBDOMAIN.workers.dev/webhooks/terra. - Save and grab the webhook signing secret, paste it into the
TERRA_WEBHOOK_SECRETsecret (you set this above, but double-check it matches what Terra shows). - Trigger a test event from the Terra dashboard. Check the Worker logs:
You should see the event arrive.wrangler tail
Set up a custom domain (optional but recommended)
api.amy.health looks better than amy-api.workers.dev, and it
isolates you from sub-domain changes.
- In Cloudflare dashboard → Workers → your worker → Settings → Triggers.
- Add a custom domain:
api.amy.health. - DNS is auto-managed if your domain is on Cloudflare; otherwise add a
CNAME api → amy-api.YOUR.workers.dev.
Update Terra and any client AMY_BASE_URL configs to the new domain.
Subsequent deploys
After the first deploy, shipping a change is:
cd cloud
bunx wrangler deployTwo minutes, zero downtime. Wrangler builds the bundle, uploads it, and Cloudflare flips the routing atomically.
Rollback
# List recent deployments
wrangler deployments list
# Roll back to a specific version
wrangler rollback <deployment-id>Rollback is instant.
Monitoring + logs
| What | How |
|---|---|
| Live tail of all requests | wrangler tail |
| Filter logs to a request ID | wrangler tail --search req_… |
| Failed events going to DLQ | wrangler queues consumer dlq amy-events-dlq |
| D1 query | wrangler d1 execute amy-db --remote --command "SELECT count(*) FROM users" |
| KV inspect | wrangler kv key list --binding=CACHE |
Cloudflare dashboard → Workers → Analytics also gives you request rate, error rate, and CPU time histograms.
Common pitfalls
"Failed to publish, no compatible runtime"
You're on the free Workers plan. Workflows and Queues require the Workers Paid plan ($5/mo). Upgrade in the Cloudflare dashboard.
Webhook returns 401
Terra signature doesn't match. Confirm TERRA_WEBHOOK_SECRET matches
what the Terra dashboard shows. Use wrangler tail to see the exact
header that arrived.
D1 query times out
D1 has per-query and per-database limits. For long queries, paginate or add indexes (see Internals: Storage).
Workflow doesn't run
Check wrangler workflows list and wrangler workflows describe TurnWorkflow. Common cause: a Workflow class isn't exported from the
Worker entrypoint.
Multi-environment (dev / staging / prod)
Edit cloud/wrangler.toml to define environments:
[env.staging]
name = "amy-api-staging"
[[env.staging.d1_databases]]
binding = "DB"
database_id = "<staging-db-id>"
# … etc
[env.prod]
name = "amy-api"
[[env.prod.d1_databases]]
binding = "DB"
database_id = "<prod-db-id>"Deploy to a specific environment:
bunx wrangler deploy --env staging
bunx wrangler deploy --env prodSecrets are per-environment too: wrangler secret put NAME --env staging.
What to read next
- Local development, the inner dev loop.
- Internals: Runtime, how Workers, Workflows, and Queues fit together once deployed.
- Architecture, the whole system in one page.
Local development
The inner loop for working on Amy itself, the Worker, the CLI, the agents, the schemas. Edit a file, see the change in <2s. No deploys, no remote anything, everything on your laptop.
Using the CLI
The amy CLI is the reference client for the Amy backend. Every command maps 1:1 to an SDK call. Every step waits for you to press enter before doing anything. Nothing happens by surprise.