Skip to main content

TL;DR

Status: BLOQUEAR LAUNCH até 1 item crítico ser resolvido. Fora isso, projeto está em estado bom — dá pra subir em ~3 dias de polimento focado. Veredicto: Engenharia já amadureceu desde a auditoria de 23/04. Tem RLS em tudo, 67 migrations versionadas, Sentry integrado, páginas legais (LGPD/CDC) muito bem escritas, Stripe live com Edge Functions. Os bloqueadores são pontuais — não estruturais. Metodologia desta auditoria (diferente da anterior):
  • npm run build rodado de verdade (passou, mas com warnings importantes)
  • npm run lint rodado (passou com erros não-bloqueantes)
  • tsc --noEmit rodado (passou limpo)
  • ✅ Supabase MCP: get_advisors security + performance + listagem de RLS em todas as tabelas
  • ✅ 4 agentes paralelos investigando segurança, fluxos críticos, prontidão produção, silent failures

🚨 BLOQUEADORES — resolver ANTES do launch

B1. Stripe LIVE secret + webhook secret committed no git history

Onde: SETUP_QUANDO_VOLTAR.md:13-14 — committed no commit 147fb2d (“feat: rebrand pra Pix do Pix + templates de email + paginas legais”) O que vazou:
STRIPE_SECRET_KEY = sk_live_51SGSOUK7r4KRXZM8M5YESsFXql51KFjN...
STRIPE_WEBHOOK_SECRET = whsec_RkWvzxSujuyENXFer8ZJXxLHOmy64Inm
Estado atual: O repositório https://github.com/gomktia/go-financas-inteligentes retornou 404 — está privado. Mas:
  • Está no git history pra sempre (a menos que faça force-push com history rewrite)
  • Qualquer colaborador atual ou futuro com acesso ao repo tem a chave
  • Se virar público algum dia, vaza
  • Backups, forks privados, espelhos — exposição lateral
Ação imediata (ordem):
  1. Stripe Dashboard → API Keys → roll o secret key live (gera novo, invalida o velho)
  2. Stripe Dashboard → Webhooks → roll o signing secret do endpoint
  3. Atualizar os secrets no Supabase (Edge Functions)
  4. Deletar SETUP_QUANDO_VOLTAR.md do filesystem
  5. Criar SETUP_QUANDO_VOLTAR.md em formato sanitizado (sem nenhum secret) e versionar
  6. Reescrever git history pra remover o arquivo (git filter-repo ou BFG Repo-Cleaner) — opcional mas recomendado
  7. Confirmar via Stripe Dashboard que a chave antiga está rejeitando requests
Por que isso é bloqueador absoluto: cobrança real envolvida. Quem tiver a chave pode emitir charges, refunds, pegar dados de clientes pagantes — não é hipotético.

⚠️ ALTO — resolver antes do launch

A1. Bug de React Hooks em produção — app/cartoes/[id]/page.tsx

Onde: linhas 87, 92, 108 O que está errado:
// linha 64-78: early returns ANTES dos hooks
if (!cartao) { return <p>Cartão não encontrado</p> }

// linha 87, 92, 108: useMemo chamados DEPOIS do early return
const gastosDoCartao = useMemo(() => ..., [...])
Lint marcou como Error: react-hooks/rules-of-hooks. Vai quebrar em runtime quando o componente entrar e sair do estado de loading. Erro do React: “Rendered more hooks than during the previous render.” Por que passou no build: Next 15 trata erros de lint como não-bloqueantes por padrão (Failed to compile. aparece no log mas exit é 0). Já causou bug em produção em outros projetos. Fix: Mover os 3 useMemo pra ANTES dos early returns, ou refatorar para componente separado.

A2. App /sentry-example-page exposta em produção

Onde: app/sentry-example-page/page.tsx Risco: Página acessível por qualquer um sem auth. Botões disparam errors reais (client + server actions). Comentário no código diz “Em produção, esta página fica acessível mas só deve ser usada por dev/QA.” — confiar em política, não em código. Consequências:
  • Qualquer um polui Sentry com erros falsos
  • Quota Sentry estourada (tem limite na conta gratuita)
  • Atacante usa pra reconnaissance (descobre internals)
  • DoS no Sentry sink
Comparação: app/dev/ofx-test/page.tsx está protegida corretamente: if (process.env.NODE_ENV !== 'development') notFound(). Aplicar o mesmo padrão. Fix: Adicionar no topo do componente (server side, não em useEffect):
import { notFound } from 'next/navigation'
if (process.env.NODE_ENV === 'production') notFound()

A3. Variáveis de ambiente não documentadas (6 críticas)

.env.local.example documenta apenas 2 mas o código usa:
  • SUPABASE_SERVICE_ROLE_KEYlib/supabase-admin.ts:8 (admin actions)
  • SENTRY_AUTH_TOKENnext.config.js:11 (upload source maps)
  • NEXT_PUBLIC_SENTRY_DSN — Sentry inits
  • NEXT_PUBLIC_SENTRY_DEBUG / SENTRY_DEBUG
  • RESEND_API_KEYlib/email.ts via Edge Function
  • STRIPE_SECRET_KEY — admin/integracoes detection
  • GOOGLE_GENAI_API_KEY — não localizei o uso, mas grep encontrou (verificar se ainda é usado ou remover)
Risco: Novo deploy quebra silenciosamente. Onboard novo dev mais lento. CI/CD pode pular validação. Fix: Atualizar .env.local.example com TODAS as vars + comentário curto explicando origem de cada uma. Adicionar uma checagem de “vars obrigatórias” em instrumentation.ts que loga aviso visível em boot se faltar.

A4. Open Redirect potencial em /auth/callback

Onde: app/auth/callback/page.tsx:51-54 O que existe:?next= e ?redirect= de query params, valida apenas com next.startsWith('/'). Bypass possível: ?next=//attacker.com/phishingstartsWith('/') é true, mas browser interpreta //domain como protocol-relative URL → vai pra attacker.com. Fix:
const safeNext = (() => {
  if (!next) return '/'
  // Bloquear // e protocol-relative
  if (next.startsWith('//') || next.includes('://')) return '/'
  if (!next.startsWith('/')) return '/'
  return next
})()

A5. Edge Runtime warnings — middleware pode quebrar

Build log mostra:
./node_modules/@supabase/realtime-js/dist/module/lib/websocket-factory.js
A Node.js API is used (process.versions at line: 35) which is not supported in the Edge Runtime.

./node_modules/@supabase/supabase-js/dist/module/index.js
A Node.js API is used (process.version at line: 24) which is not supported in the Edge Runtime.
O middleware roda em Edge Runtime e usa @supabase/ssr → que importa @supabase/supabase-js → que toca process.version. Em Vercel pode lançar runtime error em request. Fix: Já é um problema conhecido do @supabase/ssr. Verificar se a versão atual (^0.7.0) tem o fix. Se não, considerar pin em versão que evita ou aceitar risco e monitorar Sentry.

A6. app/error.tsx não reporta para Sentry

Onde: app/error.tsx:14-18 O que faz: console.error apenas em dev. Em produção: silêncio total. Consequência: Erros que caem em error boundaries de rotas (todas exceto raiz) não chegam ao Sentry. Você vê o user na tela “Algo deu errado” mas não tem stack trace. Fix:
import * as Sentry from '@sentry/nextjs'

useEffect(() => {
  Sentry.captureException(error)
  if (process.env.NODE_ENV === 'development') console.error(error)
}, [error])

A7. Sentry sem release e environment configurados

Onde: Todos os sentry.*.config.ts O que falta:
release: process.env.VERCEL_GIT_COMMIT_SHA, // ou seu próprio
environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV, // production / preview / dev
Consequência:
  • Source maps não são associados ao deploy específico → stack traces minified em prod
  • Bugs de preview branches misturam com produção no dashboard
  • Não dá pra fazer “compare entre releases”

A8. Silent failures críticos

#OndeO quêPor que importa
1lib/email.ts:27-37Email via Edge Function falha → só console.warn, retorna { ok: false } mas ninguém checa o retornoResend cair em prod = você descobre só quando user abrir ticket
2app/cartoes/_actions/ofx-actions.ts:404Assinaturas detectadas durante OFX import — se assinErr, log e segue sem retornar erroUser acha que assinaturas foram criadas mas não foram
3app/cartoes/_actions/ofx-actions.ts:137-140suggestCategoriesFromHistory() em Promise.all com .catch(() => ({}))Sugestões silenciosamente quebram
4lib/auth.ts (várias)Catches retornam { error } mas nenhuma chamada reporta pra SentryNão dá pra saber se há credential stuffing rolando
Fix padrão: Adicionar Sentry.captureException(err, { tags: { module: 'email' } }) em cada catch que hoje só loga.

📋 MÉDIO — resolver no primeiro mês pós-launch

M1. PII em logs do Sentry (LGPD-relevante)

Onde: app/cartoes/_actions/ofx-actions.ts linhas 307, 407, 416, 426, 602, 611, 695, 709, 843 Exemplo: Linha 407: console.log('ofx_assinatura_insert_success', { userId, workspaceId, created: inserted?.map((a) => a.nome) ?? [] }) → Sentry captura userId, workspaceId, e nomes “Netflix, Spotify, Claude Pro, Pix do Pix”. Por que importa para LGPD: A política de privacidade promete “Sem dados de transações em logs públicos.” Esses logs vão pro Sentry US — dado de transação financeira fora do BR sem anonimização. Multa ANPD potencial. Fix: Substituir userId por hash, valores por contadores ({ count: N }), nomes por categorias agregadas.

M2. CSRF em admin actions

Onde: Toda app/admin/_actions/*.ts Problema: Server actions aceitam userId como parâmetro sem validar Origin header. Se admin abrir um link malicioso enquanto logado, JS pode disparar resetPassword(targetUserId) ou deleteUser(). Mitigações já existentes: assertSuperAdmin() em todas as ações + middleware. Mas isso só valida que o caller é admin — não protege contra CSRF onde o admin é a vítima. Fix: Next 15 server actions já têm proteção CSRF nativa (Origin header check). Confirmar que serverActions.allowedOrigins está configurado e que requests de outros domínios são rejeitados. Adicionar test de regression.

M3. lib/supabase-admin.ts sem 'server only' enforcement

Onde: Importável de qualquer lugar. Risco: Algum dev acidentalmente importa em arquivo 'use client' → bundle final exposing service role key (catastrofe). Fix: Adicionar import 'server-only' no topo de lib/supabase-admin.ts. Vai falhar build se importado de client.

M4. Logout só local

Onde: components/auth-provider.tsx:116
const { error } = await supabase.auth.signOut({ scope: 'local' })
O que rola: Cookies/localStorage limpos no browser, mas refresh token continua válido server-side até TTL (24h). Se attacker capturou o token (XSS, malware), pode reusar. Trade-off intencional: Comentário no código diz “não bloqueia chamada de servidor que pode demorar/falhar”. OK pro UX, mas não é “logout real” no sentido de segurança. Fix nice-to-have: Server action que chama supabaseAdmin.auth.admin.signOut(userId, 'global') em background depois do local signOut. Pode demorar, mas user já saiu do app.

M5. Limites por plano: rate limit DB-level ausente

Onde: app/cartoes/_actions/ofx-actions.ts:79-98, app/crypto/_actions/crypto-actions.ts:34-76 Problema: Limite de OFX (1/free/mês) e crypto (3/free) é checado no servidor antes do INSERT, mas sem lock. Race condition possível: 11 requests paralelos com plano free passam pelo check, todas inserem. Fix: RPC PostgreSQL com SELECT ... FOR UPDATE + insert na mesma transaction. Ou unique constraint UNIQUE(user_id, date_trunc(‘month’, created_at)) onde aplicável.

M6. tabela rate_limit_log com RLS sem policies

Supabase advisor flagged: RLS Enabled No Policy em public.rate_limit_log. Estado atual: RLS habilitado mas zero policies → ninguém (exceto service role) consegue ler/escrever. Provavelmente intencional (só check_rate_limit RPC mexe), mas é warning. Fix: Documentar a intenção via policy explícita: CREATE POLICY "deny_all" ON rate_limit_log FOR ALL USING (false); OU adicionar comentário SQL na migration explicando por que está vazio.

M7. PLAN_LIMITS duplicado em 3 lugares

Onde:
  • lib/constants.ts:30 — só OFX_IMPORTS_PER_MONTH
  • hooks/use-subscription.ts:27-61PLANS completo (priceBRL, pessoal, familia, etc)
  • app/crypto/_actions/crypto-actions.ts:34PLAN_LIMITS = { free: 3, pro: 10, business: 100 } hardcoded
Risco: Mudou preço do Pro? Tem que lembrar de mudar em N lugares. Bug latente. Fix: Centralizar tudo em lib/plans.ts com tipo PlanKey e função getLimitsFor(plan). Remover duplicações.

M8. pg_net extension em schema public

Supabase advisor flagged: WARN. Fix: Mover pra schema extensions ou similar. Migration:
ALTER EXTENSION pg_net SET SCHEMA extensions;
(Validar se quebra algo antes — pg_net é usado por triggers de mensalidades reminders.)

M9. Leaked Password Protection desabilitado

Supabase advisor flagged: Auth não está checando senhas contra HaveIBeenPwned. Fix: Supabase Dashboard → Auth → Policies → habilitar “Leaked password protection”. Sem custo, sem código.

M10. Falta protection no admin route guard durante timeout

Middleware (middleware.ts:46-58) usa Promise.race com timeout 5s na RPC is_super_admin. Se RPC demora, data: null → trata como não-admin → redirect home (fail closed, OK). Mas em lib/admin-auth.ts:15-19, server actions chamam a mesma RPC SEM timeout. Inconsistência: middleware fail-closed em 5s, action espera para sempre. Fix: Adicionar timeout local na action também (ex: 10s) com fallback para AdminAuthError('FORBIDDEN').

✨ BAIXO — backlog pós-launch

#ItemOnde
17 warnings <img> ao invés de <Image />login, sidebar, header, legal-layout, auth/callback, auth/reset-password
24 errors de unescaped entities "salarios/page.tsx:142, importar-csv-sheet.tsx:525
318 console.log em código de produção fora de testesusar Sentry breadcrumbs ou remover
4Sem banner de cookies (LGPD nice-to-have já que só usa cookies necessários)criar <CookieBanner />
5Sem robots.txt nem app/sitemap.tscriar app/robots.ts e app/sitemap.ts
6app/layout.tsx sem twitter cards, icons, robots no metadataenriquecer metadata
7.gitignore não cobre *.log, .playwright-mcp/, gastos-desktop.png, AUDITORIA_TECNICA.md (root)adicionar
8.vercelignore ignora package-lock.json — risco de deploy não-reproducívelremover linha
9README.md ainda fala de localStorage e estrutura antigareescrever (já flagged em 23/04)
10DATABASE_STRUCTURE.md completamente fora da realidade — descreve schema teórico que não bate com migrations reaisdeletar ou reescrever
11Pendência declarada em SETUP_QUANDO_VOLTAR.md: emails transacionais não estão sendo chamados nas ações do appwire lib/email.ts em signup, checkout success, convite
12package.json ainda com "next": "latest"pin em versão específica
13Privacidade fala “hash bcrypt” mas Supabase usa scrypt — inconsistência menorcorrigir texto
14Privacidade promete “exportação JSON/CSV via app” — implementado?confirmar ou remover
15Privacidade fala “2FA via magic link/OTP” — magic link existe, mas 2FA propriamente dito?clarificar
16Sem npm test script no package.json (testes existem em lib/__tests__/)adicionar vitest + script
17staleTime: 30_000 copiado em ~20 hooksextrair lib/query-defaults.ts
18Magic number .limit(2000) em use-gastos.tsconstante + paginação
19Sentry: avaliar replaysOnErrorSampleRate para session replays só em erros (sem custo extra significativo)optional
20Auditoria antiga (23/04) listou A1/A2/A3 de alta severidade — ainda válidos: zero testes automatizados estruturais; service layer; app/page.tsx complexacontinuar plano da Fase 2-4

✅ O que está bem — não mexer

ÁreaEstado
RLS100% das 26 tabelas com RLS habilitado
Migrations67 migrations versionadas no Supabase, com hardening recente (fix_security_definer_search_path, protect_last_owner, rate_limit_infra, perf_composite_indexes)
Páginas legaistermos e privacidade excelentes — LGPD completa, Stripe, CDC, foro, DPO email, retenção, base legal por art. 7º
SentryBem configurado: tunnel route /monitoring (escapa ad-blockers), sendDefaultPii: false, ignore extensions, tracesSampleRate 0.1 conservador, source maps deletados após upload
Admin guardMiddleware + assertSuperAdmin() em ações + audit log em DB. Defesa em camadas.
TypeScript stricttsc --noEmit passou limpo
BuildCompila e produz bundle (com warnings, mas não falhou)
global-error.tsxReporta pra Sentry corretamente
OFX uploadValidação client-side + dedupe via hash + workspace ownership check
Dev pages/dev/ofx-test corretamente protegida com notFound() em prod
Stripe Edge Functionscreate-checkout-session, create-portal-session, webhook live configurado
Domínio pixdopix.com.brTermos, privacidade, branding consistentes

Checklist de pre-launch (em ordem)

Hoje (2-3h)

  • Rotacionar STRIPE_SECRET_KEY e STRIPE_WEBHOOK_SECRET no Stripe Dashboard
  • Atualizar secrets no Supabase Edge Functions
  • Deletar SETUP_QUANDO_VOLTAR.md ou sanitizar (sem nenhum secret)
  • Reescrever git history pra remover o arquivo (BFG ou git filter-repo)
  • Confirmar via Stripe dashboard que chave antiga foi rejeitada
  • Proteger /sentry-example-page com guard server-side
  • Fix bug useMemo em app/cartoes/[id]/page.tsx

Amanhã (3-4h)

  • Atualizar .env.local.example com todas as 7+ vars
  • Configurar release e environment no Sentry
  • Adicionar Sentry.captureException em app/error.tsx
  • Adicionar import 'server-only' em lib/supabase-admin.ts
  • Validar redirect em /auth/callback contra //attacker.com
  • Habilitar Leaked Password Protection no Supabase
  • Remover PII (emails, valores, nomes) de console.log nas ofx-actions

Depois de amanhã (2-3h)

  • Pin next em versão específica no package.json
  • Atualizar .gitignore (*.log, .playwright-mcp/, *.png de teste)
  • Remover package-lock.json do .vercelignore
  • Fix 7 warnings <img> (trocar por next/image ou suprimir lint regra)
  • Wire emails transacionais (lib/email.ts) nas ações: signup, payment confirmed, convite
  • Smoke test end-to-end em ambiente preview com Stripe test mode

Nos primeiros 7 dias pós-launch

  • Migrar para schema extensions o pg_net
  • CSRF/Origin validation em admin actions (confirmar serverActions.allowedOrigins)
  • Centralizar PLANS em lib/plans.ts
  • Setup vitest + npm test no package.json
  • Drop dos 45 índices não-usados (após confirmar baseline de uso)
  • Banner de cookies LGPD
  • Criar app/sitemap.ts e app/robots.ts

O que evoluiu desde a auditoria de 23/04

A auditoria antiga (auditoria-tecnica.mdx) listou A1/A2/A3 de alta severidade:
  • A1 — Zero testes: parcialmente resolvido. Existem 2 arquivos em lib/__tests__/ (bank-brands, competencia-fatura) + outros em lib/ofx/__fixtures__/. Mas ainda sem runner config (vitest/jest) nem npm test.
  • A2 — Acoplamento alto: ainda válido, sem mudança estrutural.
  • A3 — Dashboard complexa: ainda válido, app/page.tsx continua grande.
Adições significativas desde 23/04:
  • ✅ Sentry integrado (commit ae08edc, dd506fe)
  • ✅ Logout corrigido (commit 4b08d68)
  • ✅ Mobile scroll fix (commit 613669f)
  • ✅ Admin/integrações com OFX e Sentry card (commit 0c9655f)
  • ✅ Crypto module completo (app/crypto/)
  • ✅ OFX import (cartões + extratos)
  • ✅ Rate limit infra no DB (rate_limit_infra migration)
  • ✅ Protect last owner do workspace (impede ficar sem dono)
  • ✅ Perf composite indexes
  • ✅ Cron jobs pra mensalidades reminder
Conclusão da evolução: velocidade boa, qualidade boa. O bloqueador absoluto é só o secret no git. Resto é polimento.

Conclusão

Não é um produto que precisa de re-engenharia. Precisa de uma semana de polimento + rotacionar uma chave. Depois disso, tem condição técnica de receber clientes pagantes com confiança razoável. A estrutura de defesa em profundidade (RLS + middleware + assertSuperAdmin + audit log + Sentry) já existe e funciona — só falta tampar uns furos pontuais. Os problemas mais sérios não são arquiteturais, são higiênicos. Prazo realista pra launch: 3 dias úteis de trabalho focado nos itens BLOQUEADORES + ALTOS, depois lançar e iterar.