carlos@cesaints: ~/projects/study-platform.md — zsh

Comando: cat projects/study-platform.md

projects/study-platform.md · 3,4 KB

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

Os apps sobre as mesmas regras no servidor

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)