Instance Deployment Runbook — IJCP (2026-08-16)
Este documento é o registro de memória do deploy da primeira instância de produção real (IJCP). Ele contém: (1) inventário das instâncias, (2) o que foi feito, (3) lições aprendidas, (4) o que lembrar desta instância e (5) o runbook para futuros deploys.
Sem segredos aqui. Tokens, senhas e chaves privadas vivem fora do repo (env vars, GitHub secrets,
wrangler secret put, arquivos gitignored) — ver as seções correspondentes.
1. Inventário de instâncias
| Instância | Frontend (Pages) | Backend (Worker) | D1 | R2 | Papel |
|---|---|---|---|---|---|
| Demo | app.neemias.app (neemias) | api.neemias.app (api-neemias, workers/wrangler.toml) | neemias (c965d6fc-ee7e-41dd-bc81-65b05652aa17) | neemias-uploads | Demo pública: DEMO_MODE=true, reseed semanal (cron) |
| IJCP | ijcp.neemias.app (neemias-ijcp) | api-ijcp.neemias.app (api-ijcp, workers/wrangler.ijcp.toml) | ijcp (fa6e5e02-bfab-4b1a-8db5-daf972fd421d) | ijcp-uploads | Primeira produção real: sem demo, sem cron, licenciada |
- Conta Cloudflare:
Shaveslavers@gmail.com's Account(6ac86791e3fed01df841ff3beffd1f0f), zonaneemias.app(2b2249fc4b9b5c873eef562651d88f64). - DNS (zona
neemias.app, ambos proxied/CNAME):ijcp→neemias-ijcp.pages.devapi-ijcp→api-ijcp.shaveslavers.workers.dev
- Deploy CI:
.github/workflows/deploy-ijcp.yml(tagsijcp-v*ouworkflow_dispatch, runner self-hosted). - Durable Objects (SQLite) por worker:
CAPACITY_FEED_DO,NOTIFICATION_FEED_DO,ATTENDANCE_FEED_DO.
2. O que foi feito no deploy (2026-08-16)
- Fix crítico de auth (ADR-0031) — o PBKDF2 de duas passagens travava todo usuário após o 1º login: o cliente enviava um pre-hash com salt aleatório a cada login, então o verifier
pbkdf2:client:do servidor nunca mais conferia. Provado com probe; corrigido comclientPrehash()determinístico (salt derivado da senha) emapp/src/modules/auth/clientPrehash.ts;backendLogin()o usa. ADR-0031 corrigido. - Licenciamento — par de chaves Ed25519 de produção gerado; pública embutida em
workers/src/license/key.ts; privada emworkers/.prod-license-private.pem(gitignored, backup com o operador). Licença IJCP emitida:ijcp/nucleus,volunteers/ expira 2027-08-16. Fixtures de teste re-assinados. - Email — Cloudflare Email Sending exige Workers Paid (não disponível no plano grátis) → IJCP usa Resend (
RESEND_API_KEYsecret;RESEND_FROM_EMAIL=contato@neemias.app). O código suporta o binding[[send_email]]para um futuro upgrade de plano. - Recursos Cloudflare (via API/wrangler) — D1
ijcp, R2ijcp-uploads, Pagesneemias-ijcp; migrations aplicadas (0001-0003, roles do sistema incluídas); Workerapi-ijcp+ rota; secretsAUTH_JWT_SECRETeRESEND_API_KEY. - Frontend — build IJCP (
DEPLOY_ENV=production,VITE_BACKEND_URL=https://api-ijcp.neemias.app,VITE_DEMO_MODE=false— sobrescreve o.env.productionda demo) → Pagesneemias-ijcp. - Admin pré-criado —
admin@ijcp.neemias.app(papelADMIN, hash v1 que auto-upgrade para v2 estável no 1º login). Senha entregue fora do repo. - Deploy via CI validado — tag
ijcp-v0.1.0disparou o workflow end-to-end (migrations idempotentes → worker → secrets → Pages → domínio → health). - Roles pre-instaladas (pós-deploy) — o backend já tinha as 8 roles do sistema, mas o app lê a tabela
roleslocal (IndexedDB), que nasce vazia em produção → tela "Papéis" vazia. Fix:hydrateRoles()no login/ refresh buscaGET /api/v1/roles(admin) e faz upsert local (todas as instâncias). - Avatares DiceBear — perfis sem foto passam a renderizar avatar de iniciais gerado localmente (
@dicebear/core+@dicebear/collection, offline-safe, deterministico por nome) em lista/perfil/checkin/checkout/ turmas/formulário (todas as instâncias). - Fix da demo —
admin@neemias.local/senha123parou de funcionar (hash v2 corrompido pelo bug do item 1). Hash resetado para v1 no D1 da demo + redeploy do frontend da demo com o fix.
3. Lições aprendidas
- Salt do pre-hash do cliente tem que ser determinístico. ADR-0031 exige que o
clientHashenviado no login seja estável por senha; com salt aleatório, o 2º login falha para sempre. Sempre teste "login → logout → login" (o E2E de 1 login só não pega isso). - Rotacionar a chave de licença invalida fixtures e licenças dev.
key.tsembute a pública; após a troca, re-assine os tokens hardcoded emworkers/src/__tests__/{gating-fetch,license-route}.test.ts(comando no próprio arquivo) e re-emita licenças de clientes. - Escopo do token da Cloudflare. Um token "account-scope" (Workers/ Pages/D1/R2) não cria registros DNS (403). São necessários dois perfis: token de operação (account) + token com
Zone → neemias.app → DNS:Edit. O GitHub secretCLOUDFLARE_API_TOKENprecisa do token com DNS para o passo de DNS do workflow funcionar. - Rota de Worker ≠ DNS.
wrangler deploycom[[routes]]registra a rota, mas a hostname só resolve com um CNAME (proxied) na zona. O custom domain do Pages também precisa do CNAME ("CNAME record not set"). wrangler pages deployem tag → deployment PREVIEW. O runner faz checkout em detached HEAD; sem--branch maino deploy cai no branch "HEAD" (preview) e o domínio de produção não atualiza. Sempre usar--branch mainno workflow.- Email Sending (Cloudflare) exige Workers Paid. No plano grátis, use Resend (código já tem fallback). Verificar o domínio em resend.com/domains.
- Turbo cache e env de build.
turbo.jsonprecisa declarar na chave de cache todo env que afeta o bundle (DEPLOY_ENV,VITE_DEMO_MODE,VITE_BACKEND_URL) — senão um build da demo é reutilizado para outra instância (com demo-seed vazando para produção!). .env.productionpertence à DEMO. (VITE_DEMO_MODE=true). Instâncias de produção precisam sobrescrever comVITE_DEMO_MODE=falseno build.- Roles vivem no backend; o app espelha localmente. A tela "Papéis" lê a tabela
rolesdo IndexedDB. Instância nova → vazia. Hidratar no login (hydrateRoles) resolve para todas as instâncias. - Baselines de teste.
scripts/thresholds.sh(TEST_COUNT_BASELINE) escripts/count-as-any.shprecisam ser atualizados quando o count muda — o pre-commit falha se não. - Admin pré-criado em produção: inserir com hash v1 (
pbkdf2:) — o 1º login valida pela senha crua e faz upgrade para v2 com oclientHashdeterminístico (fica verificável para sempre). - Segredos colados no chat devem ser rotacionados após a sessão (tokens Cloudflare/Resend).
4. Memória operacional — INSTÂNCIA IJCP
Renovações (calendarizar):
Configuração viva (não mexer sem necessidade):
AUTH_JWT_SECRETdo IJCP: rotacionar derruba todas as sessões. Está emwrangler secret put(worker) + GitHub secretIJCP_AUTH_JWT_SECRET.RESEND_API_KEY(worker secret) + verificação decontato@neemias.appem resend.com/domains (sem verificação, OTP/notificações não saem).- Chave privada de licença:
workers/.prod-license-private.pem(gitignored, backup com o operador). Perder = rotacionar chave = invalidar licenças. - DNS: os 2 CNAMEs acima; o passo DNS do workflow é best-effort (falha silenciosa se o token não tiver Zone→DNS).
Deploys IJCP: push de tag ijcp-v* (ex.: ijcp-v0.1.1) ou workflow_dispatch. O workflow faz: migrations → worker → secret (se faltar) → build frontend (env IJCP) → Pages --branch main → domínio → health.
5. Runbook — FUTUROS DEPLOYS (nova instância)
- Domínios: escolher
<sub>eapi-<sub>.neemias.appna zonaneemias.app. - Config: copiar
workers/wrangler.ijcp.toml→wrangler.<id>.toml(nome do worker,[[routes]],ALLOWED_ORIGINS,ENVIRONMENT=production,LICENSE, email; sem cron/DEMO_MODE). - Recursos (token de operação):
wrangler d1 create <id>→ preencherdatabase_idno toml;wrangler r2 bucket create <id>-uploads;wrangler pages project create neemias-<id> --production-branch main. - Migrations:
npx wrangler d1 migrations apply <id> --remote --config workers/wrangler.<id>.toml. - Secrets:
AUTH_JWT_SECRET(openssl rand -hex 32),RESEND_API_KEY(se Resend) viawrangler secret put --config workers/wrangler.<id>.toml. - Worker:
npx wrangler deploy --config workers/wrangler.<id>.toml(cria a rota custom). - Frontend (online-only, guardado — #685/#703):
VITE_ONLINE_ONLY=true DEPLOY_ENV=production VITE_BACKEND_URL=https://api-<sub>.neemias.app VITE_DEMO_MODE=false pnpm build:app→bash scripts/verify-online-build.sh(falha se o flag foi omitido) →npx wrangler pages deploy app/dist --project-name neemias-<id> --branch main. - DNS (token com Zone→DNS:Edit): CNAME
<sub>→neemias-<id>.pages.deveapi-<sub>→api-<id>.shaveslavers.workers.dev(proxied). - Domínio Pages: POST
/accounts/{id}/pages/projects/neemias-<id>/domains{"name":"<sub>.neemias.app"}. - Admin pré-criado: gerar SQL com hash v1 (
hashPassword(senha, 1)viaworkers/src/modules/auth/password.ts) →INSERT users+INSERT user_roles(... 'ADMIN')no D1 remoto. Entregar a senha fora do repo. - Licença:
pnpm sign-license <id> <modulos> --plan production --key-file workers/.prod-license-private.pem→LICENSEno toml. - CI: clonar o job do
deploy-ijcp.yml(config, secrets do repo:CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKENcom DNS,<ID>_AUTH_JWT_SECRET). - Verificar: health, login ×2 (regressão do lockout), CORS do origin, CSP (
connect-srccom o novo api-*), domínios ativos no Pages. - Docs: atualizar
environments.md,production-checklist.md(seção de instância), este runbook e o CHANGELOG.
Ver também: environments.md · deployment.md · production-checklist.md · licensing.md · ADR-0031