Amy
Internals

Deploying to Cloudflare

How the Amy backend gets to prod. Maintainer runbook — the live deployment is already running at amy.heyamy.xyz; this page is for re-provisioning or standing up a staging environment.

Maintainer-only. Amy is already running in production at amy.heyamy.xyz. You only need this page if you're spinning up a fresh environment, re-running provisioning after a Cloudflare account change, or onboarding a new on-call engineer.

What you'll provision

ResourcePurposeCost at beta scale
Cloudflare WorkerThe API + webhook receiver + cron jobs$5/month (Workers Paid)
D1 databaseRelational store (users, turns, biomarkers, summaries)Free (under 5 GB)
R2 bucketLab PDFs and future exportsFree (under 10 GB)
KV namespaceIdempotency keys + SSE stream bufferFree (under 100k reads/day)
QueueAsync fan-out from Terra webhooksIncluded with Workers Paid
WorkflowsDurable agent run pipelineIncluded with Workers Paid
Cron triggersDrain unprocessed events + nightly reconcileIncluded

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 whoami

One-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-db

Output 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 CACHE

Output 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 queue

Apply database migrations

cd cloud
bunx wrangler d1 migrations apply amy-db --remote

Set 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_KEY

Verify:

wrangler secret list

Deploy

cd cloud
bunx wrangler deploy

Output ends with something like:

Published amy-api (1.23 sec)
  https://amy-api.YOUR-SUBDOMAIN.workers.dev

That 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 -5

Wire up Terra webhooks

Terra needs to know where to send events.

  1. Go to the Terra dashboard → Webhooks.
  2. Add a webhook URL: https://amy-api.YOUR-SUBDOMAIN.workers.dev/webhooks/terra.
  3. Save and grab the webhook signing secret, paste it into the TERRA_WEBHOOK_SECRET secret (you set this above, but double-check it matches what Terra shows).
  4. Trigger a test event from the Terra dashboard. Check the Worker logs:
    wrangler tail
    You should see the event arrive.

amy.heyamy.xyz looks better than amy-api.workers.dev, and it isolates you from sub-domain changes.

  1. In Cloudflare dashboard → Workers → your worker → Settings → Triggers.
  2. Add a custom domain: amy.heyamy.xyz.
  3. 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 deploy

Two 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

WhatHow
Live tail of all requestswrangler tail
Filter logs to a request IDwrangler tail --search req_…
Failed events going to DLQwrangler queues consumer dlq amy-events-dlq
D1 querywrangler d1 execute amy-db --remote --command "SELECT count(*) FROM users"
KV inspectwrangler 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 prod

Secrets are per-environment too: wrangler secret put NAME --env staging.


On this page