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
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 |
9. IJCP Production Instance (ijcp.neemias.app)
The first real production instance (the app.neemias.app/api.neemias.app pair is the public demo — DEMO_MODE=true). Everything is dedicated to IJCP:
| Resource | Value | Where |
|---|---|---|
| Worker | api-ijcp | workers/wrangler.ijcp.toml |
| Worker route | api-ijcp.neemias.app/* | [[routes]] in the toml (auto at deploy) |
| D1 database | ijcp | wrangler d1 create ijcp |
| R2 bucket | ijcp-uploads | wrangler r2 bucket create ijcp-uploads |
| Pages project | neemias-ijcp | wrangler pages project create neemias-ijcp |
| Pages domain | ijcp.neemias.app | Pages → Custom Domains (or deploy workflow) |
RESEND_API_KEY (secret) + RESEND_FROM_EMAIL | Resend (free tier) — domínio neemias.app verificado em resend.com | |
| License | LICENSE var (ijcp, nucleus+volunteers) | minted with pnpm sign-license (production keypair) |
Env vars / secrets for api-ijcp:
| Variable | Type | Value |
|---|---|---|
AUTH_MODE | Plain text | jwt (in toml [vars]) |
ENVIRONMENT | Plain text | production (in toml [vars]) |
ALLOWED_ORIGINS | Plain text | https://ijcp.neemias.app (in toml [vars]) |
ENABLED_MODULES | Plain text | * (in toml [vars]) |
LICENSE | Plain text | signed JWT (in toml [vars]) |
RESEND_FROM_EMAIL | Plain text | contato@neemias.app (in toml [vars]; verify the domain at resend.com) |
AUTH_JWT_SECRET | Secret | openssl rand -hex 32 — set via wrangler secret put AUTH_JWT_SECRET --config workers/wrangler.ijcp.toml (first deploy only; rotation logs everyone out) |
RESEND_API_KEY | Secret | Resend API key — wrangler secret put RESEND_API_KEY --config workers/wrangler.ijcp.toml |
Frontend build (must be done for IJCP, never for the demo):
DEPLOY_ENV=production VITE_BACKEND_URL=https://api-ijcp.neemias.app VITE_DEMO_MODE=false pnpm build:appVITE_DEMO_MODE=false overrides app/.env.production (which belongs to the demo instance). Deploys are automated by .github/workflows/deploy-ijcp.yml (tags ijcp-v* or manual) on the self-hosted runner; repo secrets: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, IJCP_AUTH_JWT_SECRET.
Email (Resend): IJCP envia email transacional pela Resend (plano grátis, 100/dia) — RESEND_API_KEY é secret do Worker e o domínio contato@neemias.app deve estar verificado em resend.com/domains (passo de dashboard, único). O código também suporta o binding Cloudflare Email Service ([[send_email]]), que exige Workers Paid — trocar no futuro = adicionar o binding ao toml e remover RESEND_API_KEY.
Pre-created admin: created directly in the ijcp D1 (users + user_roles rows with the ADMIN role, v1 PBKDF2 hash that auto-upgrades to the two-pass format on first login).