All-Cloudflare stack. Correcting two things up front (the code differs from the usual assumptions):
- Database is Cloudflare D1 (SQLite), accessed with a per-request Prisma client via
@prisma/adapter-d1. Not Hyperdrive/Postgres — so there is no connection string or Hyperdrive config to manage. Each environment just points at a different D1 database. - The frontend is a Cloudflare Worker (
labhive-web, built with OpenNext), not Cloudflare Pages. So there are no automatic per-branch "Pages preview URLs". The equivalent here is a stablestagingWorker on a*.workers.devURL that always points at the staging API. You deploy a branch's build to staging to preview it.
| Env | API worker | API URL | Web worker | Web URL | D1 database |
|---|---|---|---|---|---|
| local | wrangler dev |
http://localhost:8787 |
next dev |
http://localhost:3000 |
local (miniflare file) |
| staging | labhive-api-staging |
https://labhive-api-staging.your-account.workers.dev |
labhive-web-staging |
https://labhive-web-staging.your-account.workers.dev |
labhive-staging |
| production | labhive-api |
https://labhive-api.your-account.workers.dev |
labhive-web |
https://labsynch.com |
labhive |
Config lives in api/wrangler.toml and web/wrangler.toml as [env.staging] /
[env.production] blocks. The top-level config equals production (so your existing CI
and wrangler dev keep working unchanged).
Why
NEXT_PUBLIC_API_URLappears both in wrangler.toml and in build commands:NEXT_PUBLIC_*values are inlined into the browser bundle at build time. So the API URL a browser calls is fixed by the env var present duringopennextjs-cloudflare build, not by the runtime[vars]. Always build with the URL matching the env you deploy. (API_URL, used server-side, is read from[vars]at runtime.) There are no hardcoded API URLs left in the frontend — they all resolve throughweb/src/lib/api-url.ts, which readsNEXT_PUBLIC_API_URL.
Run from the repo root unless noted. These use the wrangler CLI (which talks to the
Cloudflare API for you — no dashboard clicking needed except the optional custom domain).
# 1. Create the staging D1 database
cd api
npx wrangler d1 create labhive-staging
# → copy the printed database_id and paste it into api/wrangler.toml:
# [[env.staging.d1_databases]] database_id = "PASTE_STAGING_D1_ID_HERE"
# 2. Create the staging R2 bucket (keeps staging file uploads out of production)
npx wrangler r2 bucket create labhive-files-staging
cd ..
# 3. Apply the schema/migrations to the staging D1
npm run db:apply:staging # wrangler d1 migrations apply labhive-staging --remote --env staging
# 4. Seed a starter admin into staging (same seed used for local)
npm run db:seed:staging # wrangler d1 execute labhive-staging --remote --env staging --file=./seed.sql
# 5. Staging secrets (NOT in wrangler.toml). Use a DIFFERENT AUTH_SECRET from production
# so staging tokens never validate on prod.
cd api
npx wrangler secret put AUTH_SECRET --env staging # long random string
npx wrangler secret put VAPID_PRIVATE_KEY --env staging # reuse prod's value or a new pair
npx wrangler secret put ZEPTO_TOKEN --env staging # reuse prod's, or a test token
cd ../web
npx wrangler secret put AUTH_SECRET --env staging # NextAuth session secret (any long random string)
cd ..
# 6. First staging deploy (API first, then web — see section 3)D1 runs locally via miniflare — a real local database file under .wrangler/state, no cloud
DB touched.
# once per checkout / after schema changes
npm install
npm run db:generate # generate the Prisma client
npm run db:apply:local # apply migrations to the LOCAL D1
npm run db:seed:local # seed local admin + sample data
# then start the stack (two terminals, in this order):
npm run dev:api # API on http://localhost:8787 (wrangler dev, local D1)
cd web && npm run dev # web on http://localhost:3000 (next dev)
# — or, from the root, both at once:
npm run devweb/.env.local must contain NEXT_PUBLIC_API_URL=http://localhost:8787 (and
API_URL=http://localhost:8787) so the browser build and server-side calls hit your local
API. Health check: open http://localhost:8787/api/health → {"status":"ok",...}.
Dev logins after seeding: student@ / faculty@ / admin@ example.edu, password password123.
# STAGING — migrate the staging DB, then deploy the staging worker
npm run db:apply:staging
npm run deploy:api:staging # wrangler deploy --env staging
# PRODUCTION — migrate prod DB, then deploy prod worker
npm run db:apply:prod
npm run deploy:api:prod # wrangler deploy --env productionThe API URL must be set at build time. Pick your shell:
# STAGING (PowerShell)
cd web
$env:NEXT_PUBLIC_API_URL = "https://labhive-api-staging.your-account.workers.dev"
npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy --env staging# STAGING (bash)
cd web
NEXT_PUBLIC_API_URL=https://labhive-api-staging.your-account.workers.dev npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy --env staging# PRODUCTION (PowerShell)
cd web
$env:NEXT_PUBLIC_API_URL = "https://labhive-api.your-account.workers.dev"
npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy --env productionmain→ production (automatic). Your existing GitHub Action (.github/workflows/deploy.yml) still runs on push tomain: it applies migrations to the production D1 and deploys the production API + web. Unchanged — merging tomainis your production release.- Feature branches → staging (manual). Cloudflare Workers have no automatic per-branch preview like Pages. To preview a branch, deploy it to the staging worker with the commands in section 3, then open the staging URL. Staging is a stable, shared preview env.
- (Optional) If you want branch pushes to auto-deploy to staging, add a second workflow
triggered on non-
mainbranches runningnpm run deploy:api:staging+ the staging web build/deploy. Ask and I'll write it.
[ ] 1. Locally: npm run dev → click through the change, no console/network errors
[ ] 2. Build check: cd web && npm run build (must pass, catches type errors)
[ ] 3. API → staging: npm run db:apply:staging && npm run deploy:api:staging
[ ] 4. Web → staging: build with staging NEXT_PUBLIC_API_URL, then deploy --env staging
[ ] 5. Verify staging: open the staging web URL, confirm it talks to the STAGING api
(check a request in devtools → host is *-staging.workers.dev)
[ ] 6. API → production: npm run db:apply:prod && npm run deploy:api:prod
[ ] 7. Web → production: build with prod NEXT_PUBLIC_API_URL, then deploy --env production
(or simply merge to main and let the CI do steps 6–7)
[ ] 8. Monitor: npm run tail:api:prod (live logs; watch for 5xx/timeouts)
npm run tail:api:staging # wrangler tail --env staging
npm run tail:api:prod # wrangler tail --env productionThe API now emits a structured JSON error line (route, tenant, user, message, stack) for any
unhandled error, and every DB query is bounded to 8s — so hangs surface as a clean 504 in
the tail rather than a silent stall.
Almost everything is CLI (which drives Cloudflare for you). The only true one-time actions:
- Create the staging D1:
wrangler d1 create labhive-staging→ paste thedatabase_idintoapi/wrangler.toml[[env.staging.d1_databases]]. (dashboard alt: Workers & Pages → D1 → Create database) - Create the staging R2 bucket:
wrangler r2 bucket create labhive-files-staging. (dashboard alt: R2 → Create bucket) - Set staging secrets (CLI, section 1 step 5):
AUTH_SECRET,VAPID_PRIVATE_KEY,ZEPTO_TOKENonlabhive-api-staging, andAUTH_SECRETonlabhive-web-staging. - (Optional) Staging custom domain: if you'd rather use
staging.labsynch.comthan the*.workers.devURL — Cloudflare dashboard → Workers & Pages →labhive-web-staging→ Settings → Domains & Routes → Add custom domain. (Not required; the.workers.devURL works out of the box becauseworkers_dev = truein[env.staging].) - ⚠ Production
AUTH_SECRET— verify/rotate. TodayAUTH_SECRETis a plaintext placeholder inapi/wrangler.toml [vars], so a barewrangler deploysets that dev value on production. Set a real secret and prefer--env production(which does not ship the plaintext var):Do this before switching production tocd api && npx wrangler secret put AUTH_SECRET # real random value on labhive-api
--env production, or the plaintext var is removed with no secret behind it and logins break. Your currentmain→CI path (bare deploy) keeps working either way.
Nothing else needs the dashboard — no bindings to click-create, no Pages project, no Hyperdrive.