# CODING-RULES.md — Regras de Geração de Código > Define **como escrever o código**: invariantes obrigatórias e armadilhas críticas. **Leia antes de gerar ou alterar qualquer código.** > Companion de [`STACK.md`](STACK.md) (tecnologias e versões). Documentação completa em [`docs/`](docs/) (capítulos `00`–`23`). ## 1. Invariantes (constituição — não mudam sem RFC) **Tipos & contrato** - Schema **Zod compartilhado** entre Nest e React (DTO via `createZodDto`, nunca duplicado). Schemas em `*.schema.ts`; DTOs em `*.dto.ts`; ambos em `libs/shared/api-interface`. → [§06](docs/06-validacao.md) - Erros HTTP em **RFC 9457 Problem Details**; Zod → **422** (nunca 400). 403 vira **404** quando o cliente não tem acesso (não vazar existência). → [§05](docs/05-api-design.md) - `tsconfig` base: `noUncheckedIndexedAccess`, `module/moduleResolution: NodeNext`, `experimentalDecorators` + `emitDecoratorMetadata`, `target: ES2023`. → [§01](docs/01-runtime-tooling.md) - `openapi.json` em `libs/shared/api-interface/`; CI bloqueia PR sem bump de versão na URL. → [§05](docs/05-api-design.md) **Boot, config & secrets** - `import './tracing'` como **primeiro import** de `main.ts` (OTel monkey-patch antes do Nest). → [§09](docs/09-observabilidade.md) - Config Nest validada com **Zod** (`validate: env => EnvSchema.parse(env)`) — fail-fast no boot. → [§08](docs/08-secrets-config.md) - Secrets via **HashiCorp Vault** (KV v2); runtime via Vault Agent injetando env no entrypoint do container. Nunca env hardcoded ou embutido no bundle. → [§08](docs/08-secrets-config.md) **Dados, jobs & side-effects** - Mudanças em DB via **migration Prisma** (expand/contract, backward-compatible, sem `db push` em prod); rodam **antes** do rolling deploy via URL direta (5432, sem PgBouncer). → [§03](docs/03-backend.md), [§18](docs/18-cicd-containers.md) - Trabalho assíncrono (email, webhook, processamento, agendado) **via BullMQ**; `jobId` determinístico; `queue.add` **depois** do `prisma.$transaction`. → [§11](docs/11-filas-jobs.md) - POST financeiro/crítico (pagamentos, pedidos) exige header **`Idempotency-Key`** memoizado por 24h. → [§05](docs/05-api-design.md) **Auth & rate limit** - Refresh em cookie `httpOnly; Secure; SameSite=Lax; Path=/api/v1/auth/refresh`; access token em memória (nunca `localStorage`). → [§07](docs/07-autenticacao.md) - Senha mínimo 12 chars + `zxcvbn`/`haveibeenpwned`; hash **argon2id** `m=65536, t=3, p=4` (defaults da lib `argon2@0.44.0`, centralizado no master-login). Refresh token com family revocation e hash no DB (32 bytes base64url, single-use, TTL 15-30 min). → [§07](docs/07-autenticacao.md) - Rate limit em `/login`, `/signup`, `/forgot-password`: **5/min/IP + 10/h/email** via `@nestjs/throttler` (Valkey). → [§07](docs/07-autenticacao.md), [§20](docs/20-praticas-nest-universais.md) **Frontend & UX** - Estado de servidor em **TanStack Query**; estado de formulário em **react-hook-form**; cliente em **Zustand**. Não misturar. → [§04](docs/04-frontend.md) - Upload via **MinIO presigned POST policy** (S3-compat) com `content-length-range` + `starts-with $Content-Type`; chave `users//`; nunca proxy upload/download pelo backend. → [§14](docs/14-uploads.md) - Frontend Vite servido por **Nginx em VM Proxmox + Cloudflare**; `Cache-Control: immutable` em assets versionados, `no-cache` + purge Cloudflare em `index.html`. → [§18](docs/18-cicd-containers.md) **Monorepo & flags** - Toda lib nova declara as **três tags** `scope:api|web|shared` + `type:app|domain|feature|data-access|ui|util` + `domain:|shared` ([ADR 0002](docs/adr/0002-tags-nx-canonicas.md)). Sem `layer:*` (deprecado). CI roda `pnpm nx affected -t lint` para validar boundaries. → [§02](docs/02-monorepo-nx.md) - Dependências compartilhadas (`zod`, `react`, `typescript`) via pnpm catalogs (`"zod": "catalog:"`). Named exports em libs; `index.ts` expõe a API pública. → [§01](docs/01-runtime-tooling.md), [§02](docs/02-monorepo-nx.md) - Feature flags em `libs/shared/feature-flags` com `owner` + `expiresAt` obrigatórios; CI falha em flag expirada, sem owner ou chave duplicada. Prefixos `rel-` / `exp-` / `ops-` / `perm-`. → [§10](docs/10-feature-flags.md) **Observabilidade & resiliência** - Logs com **Pino estruturado** + `redact` para `*.cpf`/`*.cardNumber`/`*.password`/`authorization`/`cookie`; sem `console.log`. → [§09](docs/09-observabilidade.md) - OTel sampling head-based 10% (`OTEL_TRACES_SAMPLER=parentbased_traceidratio`, `ARG=0.1`). Sentry React com `maskAllText: true`, `blockAllMedia: true`, `tracePropagationTargets` (W3C). → [§09](docs/09-observabilidade.md) - Métricas mínimas: p50/p95/p99, error rate, queue depth, DB pool, heap/RSS, cache hit ratio. Alertas derivados dos SLOs default por tier ([ADR 0001](docs/adr/0001-slo-defaults.md)): CRUD p99 >800ms/5min, agregação p99 >2s/5min, error >1%/5min, queue >1000/10min, DB pool >80%/5min, bounce rate do provedor de email >5% (Resend). → [§09](docs/09-observabilidade.md) - Health: `/health/live` (só `memory.checkHeap(350MB)`) + `/health/ready` (master-login + **amostra LRU** dos K pools quentes do `WorkspacePrismaPool` + Valkey + BullMQ — nunca pingar os N workspaces); dreno 15s no shutdown, `stop_grace_period: 30s` no docker-compose, HAProxy drain de 30s antes do `compose up -d`. → [§20](docs/20-praticas-nest-universais.md), [§18](docs/18-cicd-containers.md) **CI/CD & testes** - Mesma image digest promovida dev→staging→prod; tag por commit SHA, **nunca `latest`**. → [§18](docs/18-cicd-containers.md) - Actions GitHub pinadas por **commit SHA** (não tag); Trivy gate (`severity: CRITICAL,HIGH`, `exit-code: 1`); workflows com `fetch-depth: 0` + `nrwl/nx-set-shas@v4`. → [§18](docs/18-cicd-containers.md) - Pirâmide ~70% unit / ~25% integração / ~5% E2E; integração com **Testcontainers** (Postgres real `postgres:18-alpine`, Valkey quando exercitado); HTTP via `createTestClient` (`libs/api/util-testing`), nunca import direto de supertest. → [§16](docs/16-testes.md) - **Conventional Commits** (commitlint enforça): `feat, fix, chore, docs, refactor, test, perf, style, ci, build`. → [§19](docs/19-padroes-projeto.md) - Sem `console.log` / `any` / `@ts-ignore` sem justificativa. → [§17](docs/17-qualidade-codigo.md) ## 2. Pegadinhas 🔥 críticas (quebram produção / vazam dados / violam LGPD) Catálogo curto — apenas o que quebra prod, vaza dados ou viola LGPD. Pegadinhas de menor severidade (⚠️ inconsistência funcional, 💡 estilo) vivem nos capítulos canônicos em `docs/`. > **Para adicionar/editar uma pegadinha:** leia primeiro [§19b — Guia de estilo editorial](docs/19b-guia-estilo-docs.md) (template canônico, IDs `PGD--`, severidades 🔥/⚠️/💡, fluxo de contribuição). **Secrets, infra & imagem** - ❌ `PGD-SEC-001` Long-lived credentials em GitHub Secrets (`*_ACCESS_KEY`, senhas, tokens sem expiração) — use OIDC GitHub → Vault JWT auth → SSH cert efêmero (TTL 5min) ou tokens de curtíssima duração emitidos por role com `bound_claims` confinado a repo+branch. → [§18.3](docs/18-cicd-containers.md#oidc-github--vault--ssh-cert), [§08.6](docs/08-secrets-config.md#cicd-oidc-github--vault--ssh-proxmox) - ❌ `PGD-SEC-002` Secret runtime embutido em build/bundle Docker — runtime via **Vault Agent** no entrypoint do container; CI scaneia `(AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{30,}|re_[A-Za-z0-9]{20,}|hvs\.[A-Za-z0-9]+)` no `dist/`. → [§08.6](docs/08-secrets-config.md#cicd-oidc-github--vault--ssh-proxmox) - ❌ `PGD-SEC-003` `VITE_*_SECRET` / server key / SMTP / token-de-escrita em `VITE_*` — bundle é público. Permitido só `VITE_API_URL`, public keys, Sentry DSN. → [§08.8](docs/08-secrets-config.md#frontend-vite) - ❌ `PGD-CICD-001` Imagem Alpine sem `apk add --no-cache openssl` — Prisma e sharp crasham. → [§18.5](docs/18-cicd-containers.md#dockerfile--backend-nestjs-multi-stage) **Backend / Prisma** - ❌ `PGD-DB-001` `prisma migrate` via PgBouncer transaction pooling — Schema Engine quebra; use `MIGRATION_DATABASE_URL` direta (5432). → [§03.3](docs/03-backend.md#migrações) - ❌ `PGD-DB-002` Modelo de multi-tenancy errado (row-level `tenantId`, `SET search_path`, schema-per-tenant) — o stack adota **BD-por-workspace** ([ADR 0006](docs/adr/0006-multi-tenancy-bd-por-workspace.md)): cada workspace tem cluster PG próprio, resolvido via `get_workspace_connection` no master-login; modelos de domínio **não** declaram `workspaceId`/`tenantId`. → [§03.4](docs/03-backend.md#multi-tenancy) - ❌ `PGD-DB-003` `prisma` em `devDependencies` — quebra após `pnpm deploy --prod`; vai em `dependencies`. → [§03.5](docs/03-backend.md#riscos-críticos) - ❌ `PGD-DB-004` `moduleFormat = "esm"` no generator Prisma — NestJS é CJS; use `"cjs"`. → [§03.2](docs/03-backend.md#prisma) - ❌ `PGD-DB-005` `ADD COLUMN NOT NULL DEFAULT` em tabela grande — full table scan sob `AccessExclusiveLock`; use 3 deploys (nullable → backfill em job → `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL`). → [§03.3](docs/03-backend.md#1-coluna-not-null-em-3-deploys) - ❌ `PGD-DB-006` `DROP COLUMN` no mesmo PR que remove o uso — migrate roda antes do rolling swap das VMs; app v1 quebra com `P2022` durante a janela em que VM antiga ainda recebe tráfego. Faça em 2 deploys (remover uso primeiro, drop depois). → [§03.3](docs/03-backend.md#2-drop-de-coluna-em-2-deploys) - ❌ `PGD-DB-007` `RENAME COLUMN`/`RENAME TABLE` direto — app v1 ainda em tráfego usa o nome antigo. Padrão add → dual-write + backfill → drop em 3 deploys. → [§03.3](docs/03-backend.md#3-rename-em-3-deploys-add--dual-write--remove) - ❌ `PGD-DB-008` `CREATE INDEX` sem `CONCURRENTLY` em tabela quente — `ShareLock` segura todas as escritas até terminar; use `CREATE INDEX CONCURRENTLY` em migration fora de transação. → [§03.3](docs/03-backend.md#4-índice-em-tabela-quente-create-index-concurrently) - ❌ `PGD-DB-009` `import { prisma }` singleton ou `new PrismaClient()` direto em service de domínio — sob BD-por-workspace ([ADR 0006](docs/adr/0006-multi-tenancy-bd-por-workspace.md)), `PrismaClient` é resolvido por request via `cls.get('prisma')`; singleton acerta o BD errado (master-login/default) e quebra o isolamento. Lint em `libs/api/util-eslint-authz` proíbe fora de `util-prisma-workspace` e dos nomes `auditPrisma`/`billingPrisma`. → [§03.4](docs/03-backend.md#multi-tenancy) - ❌ `PGD-HTTP-001` Retornar entidades Prisma cruas — sempre mapear via Zod schema em `libs/shared/api-interface`. → [§03.3](docs/03-backend.md#type-safety-nunca-retorne-entidades-prisma-cruas) **Auth** - ❌ `PGD-AUTH-001` JWT em `localStorage`; refresh sem family revocation; `client_secret` OAuth no frontend; token de reset/refresh cru no DB. → [§07.3](docs/07-autenticacao.md#storage-no-frontend) - ❌ `PGD-AUTH-002` Cookie de refresh sem `Path=/api/v1/auth/refresh` exato — `Path` largo (`/`, `/api`) vaza credencial de 30d em logs/proxies; `Path` estreito (sem o prefixo global `/api/v1`) faz o navegador não enviar o cookie e o refresh quebra em prod (401 silencioso). → [§07.3](docs/07-autenticacao.md#refresh-rotation) - ❌ `PGD-AUTH-005` `JWT_SECRET` HS256 compartilhada entre apps cliente do master-login — uma app comprometida forja tokens válidos para qualquer outra do workspace (impersonação cross-app). Distribuir só via Vault Agent enquanto RS256 não fecha. → [§07](docs/07-autenticacao.md#guards-sem-passport) - ❌ `PGD-AUTH-007` App cliente mantendo `users.passwordHash` próprio em vez de delegar ao master-login — credenciais divergem na 1ª troca de senha + vazamento da app expõe hashes que deveriam viver só no IdP. → [§07](docs/07-autenticacao.md#password-hashing) **Autorização** - ❌ `PGD-AUTHZ-001` `findUnique({ where: { id } })` em service de domínio usando `PrismaClient` singleton — query acerta o BD errado (master-login/default) e quebra isolamento cross-workspace; sempre injetar via `cls.get('prisma')`. Defense-in-depth intra-workspace: `assertOwnership(resource, user)`. → [§23.4](docs/23-autorizacao.md#234-isolamento-por-workspace) - ❌ `PGD-AUTHZ-002` `workspace_id`/`ownerId` vindo do body/param/query do cliente — sempre do JWT (`req.user.workspace_id`, `req.user.sub`); administração cross-workspace é função do master-login, não das apps cliente. → [§23.3](docs/23-autorizacao.md#233-claims-no-jwt) - ❌ `PGD-AUTHZ-003` Decisão de autorização só no frontend — backend re-decide em **todas** as ações; UI só esconde. → [§23.8](docs/23-autorizacao.md#238-frontend-esconde-não-decide) - ❌ `PGD-AUTHZ-005` `$queryRaw`/`$executeRaw` em `PrismaClient` singleton ou em `auditPrisma`/`billingPrisma` dentro de service de domínio — atravessa workspaces sem aviso. Sempre rodar em `cls.get('prisma')`; cross-workspace deliberado usa nome explícito em `data-access-audit`/`data-access-billing`. → [§23.4](docs/23-autorizacao.md#234-isolamento-por-workspace) - ❌ `PGD-AUTHZ-006` Job BullMQ sem `workspaceId` no payload — worker não tem `req`; sem repopular o CLS (`cls.runWith({ workspaceId, prisma: pool.getOrCreate(...) }, handler)`), o handler crasha ou (com singleton acidental) escreve no BD errado. Producer sempre inclui `workspaceId`; worker sempre recria o CLS. → [§23.4](docs/23-autorizacao.md#234-isolamento-por-workspace), [§11](docs/11-filas-jobs.md) **Filas / jobs** - ❌ `PGD-FILA-001` Valkey com `maxmemory-policy` diferente de `noeviction` (LRU/LFU/random) — descarta jobs em pressão de memória. Configure `noeviction` no `valkey.conf` + alerta de memória ≥80%. → [§11.2](docs/11-filas-jobs.md#pegadinhas-críticas-leia-antes) - ❌ `PGD-FILA-002` `@nestjs/schedule` em multi-instância (>1 VM/container rodando o backend ou worker) — dispara em todas. Use BullMQ Job Schedulers (com `timeZone: 'America/Sao_Paulo'`). → [§11.2](docs/11-filas-jobs.md#pegadinhas-críticas-leia-antes) - ❌ `PGD-FILA-003` Job sem `jobId` determinístico — sem idempotência em retries / webhooks at-least-once. → [§11.9](docs/11-filas-jobs.md#idempotência) - ❌ `PGD-FILA-004` Email/webhook síncrono em handler HTTP — sempre via BullMQ. → [§11.4](docs/11-filas-jobs.md#producer), [§15.3](docs/15-email-notificacoes.md#resend--setup) - ❌ `PGD-FILA-005` `queue.add` antes do commit Prisma — `queue.add` confirma no Valkey mesmo se a transação reverter, deixando o job órfão. → [§11.4](docs/11-filas-jobs.md#producer), [§20.8](docs/20-praticas-nest-universais.md#208-use-transações-prisma-para-operações-multi-step) - ❌ `PGD-FILA-006` `@nestjs/event-emitter` para evento de domínio cross-instância — não atravessa containers/VMs, sem retry. Use BullMQ. → [§21.10](docs/21-filosofia-arquitetural.md#eventos-de-domínio-via-bullmq) **Cache & CDN** - ❌ `PGD-CACHE-001` TTL em **segundos** com cache-manager 6 — agora é **milissegundos** (breaking vs v5). → [§12.4](docs/12-cache.md#setup) - ❌ `PGD-CACHE-002` `CacheInterceptor` genérico em rotas autenticadas — chave é a URL, vaza dados entre usuários/workspaces. Use chave manual `user::...`. → [§12.6](docs/12-cache.md#uso-manual-chave-por-usuárioworkspace) - ❌ `PGD-CACHE-003` Cachear endpoints autenticados na borda (Cloudflare) / esquecer `Vary: Authorization` em cache compartilhado. Cloudflare deve bypassar `/api/*` por default. → [§12.9](docs/12-cache.md#vary--crítico) **Real-time & uploads** - ❌ `PGD-RT-001` Token em query string para auth WebSocket — vaza em access logs. Use ticket single-use 30-60s emitido por endpoint REST. → [§13.5](docs/13-realtime.md#auth-no-handshake--ticket-de-curta-duração) - ❌ `PGD-RT-002` Handler `@SubscribeMessage` lendo BD sem reconstruir CLS — o upgrade WS roda fora do pipeline HTTP, então `WorkspaceContextMiddleware` nunca executou e `cls.get('prisma')` retorna `undefined` (crash) ou, pior, importar `PrismaClient` singleton acerta o BD do master-login (`PGD-DB-009`). Padrão: para **escrita**, enfileirar BullMQ com `workspaceId` no payload (`PGD-AUTHZ-006`); para **leitura síncrona** no handler, `cls.runWith({ workspaceId: client.data.workspaceId, prisma: pool.getOrCreate(dbConfig) }, handler)` usando o `WorkspacePrismaPool` da app. → [§13.4](docs/13-realtime.md#leitura-síncrona-no-handler--clsrunwith) - ❌ `PGD-UPL-001` Presigned **PUT** sem POST policy — não trava `content-length`/`content-type`; use `createPresignedPost` com `Conditions`. → [§14.5](docs/14-uploads.md#nestjs--presigned-post-recomendado) - ❌ `PGD-UPL-002` CORS do bucket MinIO esquecido — PUT/POST quebra silenciosamente; configure via `mc` na criação do bucket e teste em CI. → [§14.12](docs/14-uploads.md#cors-do-bucket-exemplo-minio) **Observabilidade & LGPD** - ❌ `PGD-OBS-001` `import './tracing'` depois do Nest — `tracing.ts` (NodeSDK) deve ser **primeiro import** de `main.ts`. → [§09.3](docs/09-observabilidade.md#setup-mínimo) - ❌ `PGD-OBS-002` `redact` Pino sem `*.cpf`, `*.cardNumber`, `*.password`, `req.headers.authorization`, `req.headers.cookie` — LGPD exige redact agressivo. → [§09.3](docs/09-observabilidade.md#setup-mínimo) - ❌ `PGD-OBS-003` `/health/ready` pingando todos os workspaces ativos (`for (const ws of workspaces) ping(ws)`) — escala O(N) e marca o container `unhealthy` quando **um** workspace dormente está lento, retirando o nó inteiro do HAProxy por culpa de um tenant que ninguém estava usando. Sob [BD-por-workspace (ADR 0006)](docs/adr/0006-multi-tenancy-bd-por-workspace.md), readiness pinga **master-login** (obrigatório) + **amostra LRU** dos K pools mais quentes do `WorkspacePrismaPool` (K=3 default); workspaces individuais com problema vivem em `db.connections{workspace}` (alerta), não em readiness. → [§20.12](docs/20-praticas-nest-universais.md#2012-health-checks-com-nestjsterminus) **Email** - ❌ `PGD-EMAIL-001` Sem webhook do provedor (Resend) + alarme Grafana (bounce >5% / complaint >0.1%) — provedor suspende o domínio. → [§15.5](docs/15-email-notificacoes.md#bounce--complaint-handling) - ❌ `PGD-EMAIL-002` Domínio sem DKIM+SPF+DMARC publicado (Gmail/Yahoo bloqueiam desde 2024) — vale qualquer provedor. Resend gera os CNAMEs prontos. → [§15.6](docs/15-email-notificacoes.md#dkim--spf--dmarc) **Feature flags** - ❌ `PGD-FLAG-001` Decisão de negócio (preço, permissão, cobrança) avaliada só no client — sempre re-avaliar no backend; frontend só decide UI visual. → [§10.10](docs/10-feature-flags.md#backend-vs-frontend-evaluation) - ❌ `PGD-FLAG-002` Email, CPF ou telefone como atributo de targeting — hashear `userId` (SHA-256 + salt). → [§10.8](docs/10-feature-flags.md#lgpd-em-targeting) **CI/CD** - ❌ `PGD-CICD-002` Actions GitHub por tag (use SHA); tag `latest` em prod; workflow sem `fetch-depth: 0` (`nx affected` quebra). → [§18.4](docs/18-cicd-containers.md#pin-de-actions-por-commit-sha) - ❌ `PGD-CICD-003` Containerizar o frontend Vite — sirva estático via Nginx em VM + Cloudflare. → [§18.8](docs/18-cicd-containers.md#frontend-vite--não-containerizar) > Ao atualizar uma 🔥 aqui, atualize a seção §NN.x correspondente no mesmo PR. --- **Regra de ouro para o agente:** ao divergir das regras acima, **pergunte antes** e cite o capítulo de `docs/` que justifica a alternativa. Não introduza tecnologia fora de [`STACK.md`](STACK.md) sem aprovação explícita.