Cloudflare Workers Deployment#
The project uses TanStack Start + Nitro and builds straight to Cloudflare Workers. Cloudflare Workers is the only supported deployment target.
Architecture#
textBrowser ──HTTPS──▶ Cloudflare Workers (TanStack Start SSR + API)
│
├──▶ Hyperdrive ──▶ PostgreSQL (external)
├──▶ R2 Bucket (object storage, optional)
└──▶ AWS SES / Cloudflare Email (mail, optional)
- Frontend SSR and all
/v1/*API routes run in the same Worker - PostgreSQL is accessed via Cloudflare Hyperdrive (pooling + edge caching)
- The database can live anywhere reachable: Supabase / Neon / Railway / self-hosted
Prerequisites#
| Tool | Purpose |
|---|---|
| Cloudflare account | Workers + Hyperdrive |
wrangler CLI | npm i -g wrangler |
| PostgreSQL | Any publicly reachable instance (Supabase / Neon recommended) |
One-time Setup#
1. Login to Cloudflare#
bashwrangler login
2. Create Hyperdrive (DB connection pool)#
Go to Cloudflare Dashboard → Workers & Pages → Hyperdrive → Create configuration:
- Name: anything you like, e.g.
stackflare-db - Connection string: your direct PostgreSQL URL,
postgresql://USER:PASSWORD@HOST:5432/DBNAME - Caching: enabled by default (recommended)
- Click Create
After creation, the detail page shows a Hyperdrive ID (looks like abc123def456...) — copy it for the next step.
CLI also works:
bashwrangler hyperdrive create stackflare-db \ --connection-string="postgresql://USER:PASSWORD@HOST:5432/DBNAME"But the dashboard gives you visibility into connection status and cache hit metrics.
3. Edit wrangler.jsonc#
jsonc{
"name": "your-worker-name", // ← your Worker name
"compatibility_date": "2026-06-10",
"compatibility_flags": ["nodejs_compat"],
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "abc123def456...", // ← id from previous step
"localConnectionString": "postgresql://..." // ← only used by `wrangler dev`
}
]
}
4. Push Secrets (one command)#
Put your env vars in the project's root .env (reference .env.example), then push everything to Workers in one shot:
bashwrangler secret bulk .env
Same command in CI (just pass the token):
bashCLOUDFLARE_API_TOKEN=$CF_API_TOKEN wrangler secret bulk .env
Re-run the same command whenever
.envchanges.
5. Run Database Migrations#
Migrations run outside the Worker — execute from somewhere that can reach the DB directly (local machine or CI runner):
bashPOSTGRES_CONNECTION_STRING="postgresql://USER:PASSWORD@HOST:5432/DBNAME" \
npm run db:migrate
⚠️ Run migrations before every deploy, since code changes may ship new SQL (in
src/server/migrations/). The recommended setup is to makedb:migratea mandatory step in your CI/CD pipeline — don't rely on people remembering.
Build & Deploy#
Manual (local)#
bashNITRO_PRESET=cloudflare-module npm run build
wrangler deploy
Or as one line:
bashNITRO_PRESET=cloudflare-module npm run build && wrangler deploy
CI/CD (recommended)#
Auto-deploy on push to main. Order matters: run db:migrate first, then wrangler deploy, so the schema is ready when new code goes live.
Example GitHub Actions (.github/workflows/deploy.yml):
yamlname: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: npm ci
# 1. Database migration (must run before every deploy)
- name: Run DB migrations
run: npm run db:migrate
env:
POSTGRES_CONNECTION_STRING: ${{ secrets.POSTGRES_CONNECTION_STRING }}
# 2. Build + deploy Worker
- name: Build
run: npm run build
env:
NITRO_PRESET: cloudflare-module
- name: Deploy
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Add these in GitHub → Settings → Secrets and variables → Actions:
POSTGRES_CONNECTION_STRING— direct DB URL (CI uses it for migrations)CLOUDFLARE_API_TOKEN— create one, template "Edit Cloudflare Workers"CLOUDFLARE_ACCOUNT_ID— bottom-right of Cloudflare Dashboard home
Successful deploy output looks like:
arduinoPublished your-worker-name (1.23 MiB)
https://your-worker-name.<account>.workers.dev
Custom Domain#
Add routes in wrangler.jsonc:
jsonc"routes": [
{ "pattern": "yourdomain.com", "custom_domain": true },
{ "pattern": "www.yourdomain.com", "custom_domain": true }
]
If your DNS is on Cloudflare, this is automatic. wrangler deploy again to apply.
Local Preview#
Three dev modes for different needs:
| Mode | Command | Runs on | DB connection | Secrets | When to use |
|---|---|---|---|---|---|
| Vite dev (fastest) | npm run dev | local Node | .env direct | .env | UI / business code, fastest HMR |
| Workers local | wrangler dev | local workerd | wrangler.jsonc localConnectionString (direct) | local .dev.vars or wrangler.jsonc | Verify Workers runtime behavior |
| Workers remote | wrangler dev --remote | Cloudflare edge | real Hyperdrive | production secrets | Verify production reality (DB, email, R2, etc.) |
Day-to-day, npm run dev covers 80% of cases. Switch to wrangler dev --remote to verify real Hyperdrive connectivity, Stripe webhook callbacks, R2 bindings, etc.
Troubleshooting#
500 with "missing nodejs_compat"?
Check wrangler.jsonc compatibility_flags includes "nodejs_compat".
Hyperdrive can't connect?
- Ensure the source DB allows Cloudflare IPs (Supabase/Neon allow by default)
- Re-run
wrangler hyperdrive listto confirm the id
Email not sending?
Check MAIL_SEND_ENABLED=true and the provider's secrets are set.
Stripe Webhook signature mismatch?
Make sure STRIPE_WEBHOOK_SECRET_KEY is the endpoint signing secret (whsec_...), not the API key.