Production Deployment Checklist
app.neemias.app and api.neemias.app are production environments. Local
pnpm devis development-only. This checklist covers the full production setup — Cloudflare Dashboard + code.
1. Environment Variables — Cloudflare Dashboard
These must be set manually in the Cloudflare Dashboard. They are never committed to the repository.
Cloudflare Workers (api.neemias.app)
Navigate to Workers & Pages → neemias → Settings → Variables.
| Variable | Type | Required | Description |
|---|---|---|---|
AUTH_MODE | Plain text | Yes | Set to jwt. Default is dev (accepts any token — insecure). |
AUTH_JWT_SECRET | Secret | Yes | Generate with openssl rand -hex 32. Used to sign/verify JWT tokens. |
ENVIRONMENT | Plain text | Yes | Set to production. Controls dev-only endpoints like /api/v1/_seed. |
Optional (dev-mode tokens): If you need local development against the remote API, also set these plain-text variables:
AUTH_DEV_ADMIN_TOKEN— e.g.dev-admin-tokenAUTH_DEV_CALLER_TOKEN— e.g.dev-caller-tokenAUTH_DEV_COORDENACAO_TOKENAUTH_DEV_ADMINISTRATIVO_TOKENAUTH_DEV_VOLUNTARIO_TOKEN
Cloudflare Pages (app.neemias.app)
Navigate to Workers & Pages → neemias-app → Settings → Environment Variables.
| Variable | Type | Required | Description |
|---|---|---|---|
DEPLOY_ENV | Plain text | Yes | Set to prod. Without this, the app's seed runs automatically. Frontend seed guard checks === \"prod\". |
2. Deploy Pipeline
# 1. Frontend — build + Pages auto-deploy
pnpm build:app
git add -A && git commit -m "..."
git push
# → Cloudflare Pages builds and deploys automatically
# 2. Worker — direct deploy
pnpm deploy:workerWhat build:app does
- Runs
scripts/generate-headers.ts(withNODE_ENV=production) — writes the CSP viaapp/csp.config.tsintoapp/public/_headers. In production, this uses'unsafe-inline'CSP (required because Cloudflare injects inline<style>via font proxy and inline<script>via JS Challenge at the edge). - Runs Vite production build (Rolldown bundler).
- Output goes to
app/dist/. Cloudflare Pages serves from this directory.
What deploy:worker does
- Bundles
workers/src/viawrangler deploy. - Applies
wrangler.tomlbindings (D1 database, R2 bucket). - Deploys to
api-neemias.shaveslavers.workers.dev(and custom domain).
3. DNS — Custom Domains
Configured in Cloudflare Dashboard → Workers & Pages → (select service) → Custom Domains.
| Domain | Service | Type |
|---|---|---|
app.neemias.app | neemias-app (Pages) | Frontend SPA |
api.neemias.app | neemias (Worker) | Backend API |
Both require an A/AAAA/CNAME record in the Cloudflare DNS zone.
4. Database Migrations
# Local (development D1)
pnpm db:migrate:local
# Remote (production D1)
pnpm db:migrate:remoteMigrations live in migrations/0001_*.sql through migrations/00XX_*.sql. The Worker reads migrations_table in D1 to track which have been applied.
5. Seed Data
# Populates dev D1 with demo data (only works with AUTH_MODE=dev)
curl -X POST https://api.neemias.app/api/v1/_seed \
-H "Authorization: Bearer <DEV_TOKEN>"⚠️ Never run seed in production — it creates default admin accounts. The seed endpoint is blocked when ENVIRONMENT=production.
6. First-Time Production Setup (step by step)
flowchart TD
A[Clone repo] --> B[Set env vars in Dashboard]
B --> C{pnpm db:migrate:remote}
C --> D{pnpm deploy:worker}
D --> E[Verify API health]
E --> F{pnpm build:app}
F --> G[git push → Pages deploy]
G --> H[Verify app login]
H --> I((✅ Done))Quick reference
| Step | Command / Action | Expected result |
|---|---|---|
| 1. Env vars | Cloudflare Dashboard → Workers → Variables | AUTH_MODE=jwt, AUTH_JWT_SECRET set |
| 2. Env vars | Cloudflare Dashboard → Pages → Variables | DEPLOY_ENV=prod |
| 3. Migrate DB | pnpm db:migrate:remote | "Executed N commands" |
| 4. Deploy Worker | pnpm deploy:worker | "Published neemias" |
| 5. Health check | curl https://api.neemias.app/api/v1/health | {"status":"ok"} |
| 6. Build frontend | pnpm build:app | "built in Xs" |
| 7. Deploy frontend | git push | Pages build succeeds |
| 8. Login test | Visit app.neemias.app | Login page loads |
7. Production Security Checklist
- [ ]
AUTH_MODE=jwt(notdev) - [ ]
AUTH_JWT_SECRETis a strong random value stored as a Secret (not plain text) - [ ]
ENVIRONMENT=production(blocks seed endpoint) - [ ]
DEPLOY_ENV=prod(on Pages — blocks frontend seed, checked as=== "prod"in code) - [ ] CSP uses production mode — no
'nonce-...'in CSP (nonces are dev-only) - [ ] CSP includes
'unsafe-inline'instyle-srcandscript-src(required for Cloudflare font proxy + JS Challenge) - [ ]
NODE_ENV=productionwas exported duringpnpm build:app - [ ]
scripts/pre-deploy-check.shexits 0 (validates CSP mode + required origins) - [ ]
scripts/validate-csp.shpasses (all required origins in connect-src) - [ ]
frame-ancestors 'none'in CSP (prevents clickjacking) - [ ]
Strict-Transport-Security: max-age=63072000is active - [ ] No dev-mode tokens exposed in production
- [ ] All D1 migrations applied (
pnpm db:migrate:remote) - [ ] Rollback tag identified (previous
v*tag for quick revert) - [ ] D1 database is backed up (export from Dashboard)
- [ ] Visual smoke test: login page loads, styles render, no CSP console errors
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Login fails, console shows CSP errors, app crashes | Dev-mode nonce leaked into production CSP — 'unsafe-inline' ignored | Run NODE_ENV=production pnpm build:app to regenerate _headers |
| Login page renders unstyled but no JS error | Same CSP issue — inline styles blocked | Same fix: NODE_ENV=production pnpm build:app |
| Login fails, no CSP errors | AUTH_MODE=dev but no dev token set | Set AUTH_MODE=jwt or configure dev tokens |
| Seed creates admin on prod | ENVIRONMENT not set | Set ENVIRONMENT=production in Worker env vars |
| Pages build fails | Missing VITE_BACKEND_URL env | Set to https://api.neemias.app in Pages build settings |
| API returns 403 | JWT expired or invalid | Check AUTH_JWT_SECRET matches between Worker and client |