Comando: cat projects/study-platform.md
Plataforma de estudos com pagamento, acesso e provas cronometradas
Produto próprio: site de venda, aplicação do aluno e painel de operação, três apps num monorepo. As regras que mexem com dinheiro, acesso e dados pessoais ficam no servidor, em pacotes compartilhados pelos três, com testes contra Postgres real. Está publicado e em pré-lançamento comercial; este case trata da engenharia, não de tração.
abrir demonstraçãosimulação do sistema com dados fictícios · 6 telas
- Papel
- Fundador técnico e arquiteto
- Período
- mai/2026 – hoje
- O que eu fiz
Eu: Arquitetura, modelo de dados, invariantes de negócio, escolha de trade-offs, revisão das mudanças críticas e operação.
Projeto privado, descrito sem identificar cliente, produto ou empresa.
Diagrama
O site de venda, a aplicação do aluno e o painel de operação, com 2FA obrigatório, chamam por Server Actions os mesmos pacotes do servidor, onde ficam as regras de preço, acesso, cobrança, segurança, geração de questões e fiscal. Os pacotes gravam no PostgreSQL com RLS, recebem o webhook assinado do checkout hospedado no provedor de pagamento e pedem questões a um modelo de linguagem com saída validada. Toda questão gerada nasce inativa e só chega ao aluno depois da revisão humana.
Contexto e problema
Quem estuda para concurso compra material genérico e não sabe se está pronto para aquela prova. O produto vende acesso por concurso e aplica provas cronometradas comparáveis entre alunos. Para isso precisa cobrar sem confiar no navegador, liberar e revogar acesso sem erro de dinheiro e usar IA para produzir questões sem publicar nada que uma pessoa não tenha revisado.
Arquitetura
Monorepo com três apps Next.js (site de venda e conteúdo, aplicação do aluno, painel de operação) e pacotes compartilhados. As regras críticas, como preço, acesso, cobrança, segurança, geração de conteúdo e fiscal, ficam nos pacotes, e as três telas usam exatamente a mesma lógica.
- Dados: PostgreSQL com migrações versionadas. Tabelas sensíveis novas nascem com RLS e com os privilégios dos papéis anônimos revogados.
- Identidade: instâncias de autenticação separadas para aluno e admin, com segredos e cookies distintos, bloqueio por tentativas e 2FA obrigatório no admin.
- Dinheiro: preço calculado no servidor, checkout hospedado no provedor, webhook assinado e idempotente, estorno e chargeback revogando o acesso na mesma transação.
- Prova cronometrada: cronômetro, sorteio e correção no servidor, com trava transacional para que duas provas simultâneas não gerem dois créditos de ranking.
- Geração de questões e leitura de provas: geração de questões e extração de provas em PDF com saída estruturada revalidada por schema; o texto do PDF é tratado como dado não confiável; custo máximo reservado antes de cada chamada.
- Jobs: rotinas idempotentes para LGPD, campanhas em lotes e fila fiscal com lease, cada uma com batimento para alerta por ausência.
Desafios
- Documentação otimista. A documentação dizia que as nove regras de negócio críticas estavam íntegras. Uma auditoria linha a linha mostrou que só duas estavam sem ressalva. Os quatro furos de dinheiro e acesso foram corrigidos em seis dias, com um ADR para cada.
- CI verde que validava pouco. O type-check não rodava nos apps e a suíte e2e não executava nada. Em paralelo, o compilador estourava a pilha num tipo gerado pelo ORM e escondia 45 erros; a causa foi isolada testando variantes uma a uma.
- Latência que nenhum código resolveria. A medição apontou a distância entre função e banco, e não as consultas.
- Oferta sem entrega. O checkout aceitava um item sem material publicado. A solução foi um predicado único de “oferta entregável”, usado pela publicação, pelo checkout e pelo catálogo.
Solução
As regras de dinheiro, acesso e dados pessoais saíram das telas e foram para pacotes testados contra Postgres real. Cada uma virou invariante com dono, teste e um portão de revisão obrigatório que pode bloquear a mudança. Quando a falha era de concorrência, a correção usou a primitiva do banco (operação condicional, trava transacional, releitura dentro da transação) em vez de lock externo. A entrega passou a ser controlada pelo CI e por um portão local que exige os checks no commit exato e uma revisão separada da implementação.
O que eu faria diferente
Escreveria os testes de concorrência antes das telas, e não depois da auditoria. E trataria desde o primeiro dia a documentação de estado como algo que precisa de prova e data: foi ela que escondeu os furos por mais tempo.
Restrições
- Uma pessoa, orçamento baixo e serviços gerenciados.
- Funções serverless com uma conexão por função, então cada consulta extra vira latência em série.
- Acesso pago sem erro de dinheiro, nos dois sentidos.
- Exclusão de dados pessoais sem apagar o registro fiscal da venda.
- Nenhuma questão gerada automaticamente chega ao aluno sem revisão humana.
Decisões
Funções na mesma região do banco
- Contexto
- Uma consulta trivial levava centenas de milissegundos porque a função rodava em outro continente, e cada tela somava várias idas e voltas.
- Escolha
- Fixar a região de execução junto do banco antes de otimizar consultas.
- Ganhos
- Resolve a causa medida sem reescrever código.
- Custos
- Dependência de uma única região.
Claim atômico do evento de webhook
- Contexto
- Duas entregas simultâneas do mesmo evento de pagamento podiam liberar ou revogar acesso duas vezes.
- Escolha
- Registrar o evento com uma operação condicional no banco antes de processar, em vez de consultar e depois gravar.
- Ganhos
- Idempotência garantida pelo próprio banco, sem lock externo.
- Custos
- Um claim sem conclusão precisa de expiração e reprocessamento.
Rate limit por criticidade
- Contexto
- Se a infraestrutura de limite cair, abrir tudo é perigoso e fechar tudo derruba o produto.
- Escolha
- Redis primeiro, depois um backstop no Postgres, e fail-closed só para credencial e dinheiro.
- Ganhos
- Queda parcial não abre brute force nem derruba as rotas comuns.
- Custos
- Rotas caras de geração automática ainda ficam do lado permissivo até nova decisão.
CSP em enforce com nonce por requisição
- Contexto
- A política antiga estava em modo de relatório e não bloqueava nada.
- Escolha
- CSP com nonce e strict-dynamic emitida no middleware.
- Ganhos
- Script injetado não executa.
- Custos
- As páginas passaram a ser renderizadas sob demanda.
Stack e por quê
- Next.js (App Router) nos 3 apps
- Server Components e Server Actions mantêm as regras no servidor.
- Turborepo e pnpm
- Preço, acesso, cobrança e segurança vivem em pacotes usados pelos três apps.
- PostgreSQL e Prisma
- Migrações versionadas e primitivas do banco para concorrência.
- Auth.js com TOTP
- Duas instâncias isoladas (aluno e admin) e 2FA obrigatório no painel.
- SDK de LLM com saída validada por schema
- Questão gerada nasce inativa e passa por revisão humana.
- Vitest, Playwright e GitHub Actions
- CI com 8 jobs: lint, tipos, guardas de regressão, auditoria de dependências, testes com Postgres, build, e2e e gate.
Resultados
3 apps, mais de dez pacotes internos, cerca de 109 mil linhas de TypeScript, 59 modelos e 64 migrações.
repositório privado auditadoContagem no repositório(Auditoria do repositório privado, set/2026)167 arquivos de teste unitário e de integração e 24 specs e2e.
repositório privado auditadoContagem no repositório(Auditoria do repositório privado, set/2026)Consulta trivial caiu de 578 ms para 21–34 ms depois de fixar a região.
repositório privado auditadoMedição registrada em ADR(ADR do projeto (não reexecutado nesta auditoria), set/2026)Site no ar com CSP em enforce (nonce e strict-dynamic), HSTS com preload e headers de segurança.
repositório privado auditadoHeaders conferidos em produção(Inspeção passiva dos headers, set/2026)
Ângulo de segurança
Superfície de ataque
- Server Actions tratadas como endpoints públicos.
- Webhooks de pagamento e crons.
- Links assinados para conteúdo pago.
- Painel administrativo e fluxos de conta (2FA, reset de senha).
Controles implementados
- Preço calculado no servidor; checkout hospedado no provedor.
- Webhook com assinatura verificada no corpo cru e claim atômico.
- Conteúdo pago servido por link assinado de curta duração, conferido a cada requisição.
- 2FA imposto na fronteira das Server Actions, não só no layout.
O que eu testaria hoje
- Corridas em créditos, ranking e tokens de uso único com requisições paralelas.
- Troca de identificadores entre contas (IDOR) nas Server Actions.
- Revogação de acesso em estorno e chargeback de ponta a ponta.
Evidências
- repositório privado auditadoRepositório privado auditado(Auditoria do repositório privado, set/2026)