O SaaS de recibos guarda recibos: cliente, valor, data, e o CPF ou CNPJ de quem recebe. Antes de escrever uma linha de código, decida o A forma esperada dos dados de uma tabela: quais colunas existem e de que tipo cada uma é. É contrato. das tabelas. Trecho do plano (feat-recibos-foundation.md):
Key architectural decisions:
VARCHAR(14) no campo de documento: guarda CPF (11) e CNPJ (14) sem máscara; CNPJ alfanumérico chega em julho de 2026. Projete pra isso desde o dia um.
Valor em centavos (BIGINT), nunca float: dinheiro em float acumula erro de arredondamento.
criado_em como TIMESTAMPTZ: recibo tem data fiscal; fuso errado vira bug de relatório.
Cada decisão fechou uma porta de retrabalho futuro. Por exemplo: usar INTEGER no campo de documento teria parecido inocente em janeiro de 2026 e quebrado em julho, quando o governo passa a permitir letras no CNPJ. Migrar uma tabela cheia depois custa horas. Decidir agora custa 30 segundos.
Você vai tomar a mesma decisão na escala do seu produto.
Refatorar schema depois custa muito mais do que decidir bem agora. Adicionar coluna num banco com 1000 registros custa 30 segundos; renomear coluna nesse mesmo banco com 8 arquivos que usam ela leva 1-2 horas. Decidir agora poupa esse retrabalho.
A primeira tarefa que o /work propôs na L01 foi (provavelmente) o schema da tabela principal da fatia 1. Os passos abaixo vão do schema proposto até a Arquivo SQL (ou equivalente) que muda a forma do banco: cria tabela, adiciona coluna, renomeia campo. É versionado: a primeira é 0001, a segunda 0002, e o banco aplica todas em ordem. aplicada no banco local.
Volta no /work do final da L01. Antes de digitar y, o Claude mostra o conteúdo do arquivo de schema que vai criar:
saída esperada
Vou criar o arquivo de schema (ex: db/schema/recibos.ts no Drizzle):
import { pgTable, serial, varchar, bigint, timestamp } from 'drizzle-orm/pg-core'
export const recibos = pgTable('recibos', {
id: serial('id').primaryKey(),
contador_id: integer('contador_id').notNull(),
cliente_cpf: varchar('cliente_cpf', { length: 11 }).notNull(),
valor_centavos: bigint('valor_centavos', { mode: 'number' }).notNull(),
numero_sequencial: integer('numero_sequencial').notNull(),
criado_em: timestamp('criado_em').defaultNow().notNull(),
})
Do you want to proceed? [y/n]
O exemplo usa Drizzle. Se seu projeto usa Prisma ou outro ORM, o output muda, mas as 5 perguntas abaixo valem igual.
Aplique as 5 perguntas brasileiras ao schema antes de aprovar:
Tipos batem com o domínio brasileiro?cliente_cpf é varchar(11) (CPF tem 11 dígitos sem máscara), não int (perde zero à esquerda) nem varchar(14) (assume formato com pontos). Se vai aceitar CNPJ também, varchar(14) faz sentido. Decide agora.
Valores monetários sãoRegra de ouro no Brasil: armazene valor monetário em centavos como integer (100 em vez de 1.00). Float acumula erro de arredondamento: 0.1 + 0.2 em JavaScript dá 0.30000000000000004., não float? Float em dinheiro é onde produto brasileiro pega bug fiscal. R$ 10,30 como 10.3 vira 10.299999... depois de duas multiplicações. Se o Claude propôs float ou decimal pra valor, peça pra trocar pra bigint.
Cada FK tem o references() correto? No exemplo, contador_id deveria referenciar a tabela de contadores. Se ela ainda não existe, isso vai dar erro na migration. Solução: cria as duas no mesmo schema, ou deixa sem FK por enquanto e adiciona quando a outra tabela existir.
Tem criado_em / atualizado_em em colunas timestamp? Quase todo produto eventualmente vai querer esses campos pra debug e auditoria. Adicionar depois requer migration. Adicionar agora é uma linha.
Naming convention bate com o resto do projeto? Se o resto do código usa camelCase, não misture snake_case. Pegue uma convenção e fique nela.
Quando o schema passa nas 5 perguntas, aprova com y. Claude cria o arquivo de schema e depois propõe gerar a migration:
saída esperada
Arquivo criado. Agora vou gerar a migration.
Vou rodar:
npm run db:generate (Drizzle Kit gera SQL a partir do schema)
Output esperado: arquivo em drizzle/migrations/0001_*.sql
com o CREATE TABLE.
Do you want to proceed? [y/n]
O comando exato muda conforme o ORM (npx prisma migrate dev, npx drizzle-kit generate). O CLAUDE.md do projeto deve ter documentado.
Aprove. Olhe o arquivo SQL gerado antes de aplicar no banco:
prompt · text
Mostra o conteúdo do arquivo de migration mais recente que você acabou de gerar. Quero conferir o SQL antes de aplicar.
Coisas que valem checar no SQL bruto:
NOT NULL nas colunas certas: esquecer em coluna que não deveria aceitar null é bug fácil de pegar agora, difícil de pegar em produção.
Tipos batem: BIGINT pra monetário, VARCHAR(11) pra CPF, TIMESTAMP WITH TIME ZONE se você espera lidar com múltiplos fusos.
Defaults: criado_em deve ter DEFAULT NOW(), não vir vazio.
Comita os dois arquivos: schema (db/schema/recibos.ts) e migration (drizzle/migrations/0001_*.sql).
prompt · text
Faça commit dos arquivos de schema e migration que acabamos de criar. Stage os dois arquivos, com mensagem `feat(db): add recibos table` (substitua "recibos" pelo nome real). Mostre os comandos antes de executar.
O CEAP consome dados abertos da Câmara. A seção "Database Schema" do plano descreve as tabelas de despesa com tipos e relacionamentos completos. Trecho da tabela de despesas:
Duas decisões tomadas antes de qualquer SQL rodar:
varchar(7) pra id_deputado (não int). IDs vêm com zeros à esquerda significativos: 0012345 é diferente de 12345. int apagaria os zeros.
decimal(14, 2) pra valor_liquido, não float. O valor vem da Câmara já com vírgula. decimal preserva precisão; float arredonda. Aqui a escolha foi decimal em vez de bigint-em-centavos porque a fonte já decidiu o formato. Decisão consciente, registrada no plano.
A decisão de schema fica no plano antes da migration. Quando o /work propôs cada CREATE TABLE, dava pra checar contra o ERD documentado, sem retrabalho.
Schema é decisão de produto, não detalhe de implementação. Decide os tipos pensando nos dados que a tabela vai aceitar, antes do código rodar.
5 perguntas brasileiras antes de aprovar: tipo bate com domínio? Valor monetário em bigint centavos? FK referencia a tabela certa? Tem timestamps? Naming convention bate?