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#

ToolPurpose
Cloudflare accountWorkers + Hyperdrive
wrangler CLInpm i -g wrangler
PostgreSQLAny publicly reachable instance (Supabase / Neon recommended)

One-time Setup#

1. Login to Cloudflare#

bashwrangler login

2. Create Hyperdrive (DB connection pool)#

Go to Cloudflare DashboardWorkers & PagesHyperdriveCreate configuration:

  1. Name: anything you like, e.g. stackflare-db
  2. Connection string: your direct PostgreSQL URL, postgresql://USER:PASSWORD@HOST:5432/DBNAME
  3. Caching: enabled by default (recommended)
  4. 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 .env changes.

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 make db:migrate a 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

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_TOKENcreate 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:

ModeCommandRuns onDB connectionSecretsWhen to use
Vite dev (fastest)npm run devlocal Node.env direct.envUI / business code, fastest HMR
Workers localwrangler devlocal workerdwrangler.jsonc localConnectionString (direct)local .dev.vars or wrangler.jsoncVerify Workers runtime behavior
Workers remotewrangler dev --remoteCloudflare edgereal Hyperdriveproduction secretsVerify 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 list to 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.