Files
agenciapsilmno/novo-rumo.txt
T
Leonardo a73b82fa86 freemium F1: enforcement de limite de pacientes (schema-per-tenant)
- therapist_free ganha max_patients=20 (clinic_free ja tinha 30)
- trigger BEFORE INSERT em patients le plan_features.limits em runtime,
  resolve tenant por TG_TABLE_SCHEMA, plano ativo (clinica via tenant_id +
  pessoal via owner user_id), conta vivos (status<>Arquivado) e da RAISE
  PLAN_LIMIT_REACHED|patients|<n>
- helpers tenant_active_plan_id / plan_feature_limit (globais, sobrevivem F6.3)
- wiring: tenants novos ganham via trg_attach_business_triggers; 9 existentes backfill
- testado: clinic_free bloqueia em 30, therapist_free em 20, PRO ilimitado (rollback)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:05:19 -03:00

150 lines
10 KiB
Plaintext

---
# TAREFA: Implementar modelo freemium/PLG (plano gratuito self-service + Upgrade PRO)
Você vai transformar o caminho de aquisição de assinatura deste SaaS multi-tenant
em um modelo freemium/PLG, igual ao que já fiz num sistema irmão. O objetivo:
qualquer visitante cria uma conta gratuita sozinho, confirma o e-mail, e o ambiente
do tenant é provisionado automaticamente — sem dev no meio. Plano gratuito limitado
+ botão "Upgrade PRO" no topo.
IMPORTANTE: este sistema é PARECIDO mas NÃO idêntico ao de referência. NÃO assuma
nomes de tabelas/funções/rotas. Antes de QUALQUER código, faça a fase de descoberta
e me apresente o mapa + as decisões pra eu confirmar. Trabalhe em fases, commitando
por assunto, e validando cada migration no banco local em transação com ROLLBACK
antes de seguir. Rode o build a cada bloco de frontend.
## FASE 0 — DESCOBERTA (não codar ainda; me devolva um mapa com file:line)
Mapeie e me explique como funciona hoje:
1. Landing page / vitrine de planos e como o signup é acionado (query params? rota?).
2. Fluxo de signup: componente, se usa supabase.auth.signUp direto ou um wrapper,
o que cria (auth user, profile, tenant, subscription). Existe trigger
handle_new_user em auth.users? Onde o profile nasce e com qual role default?
3. Modelo de planos SaaS: tabelas (plans, plan_prices, plan_features, plan_limits,
subscriptions, subscription_intents...), e o catálogo de features atual (LEIA
DO BANCO, não de seeds antigos — o catálogo costuma divergir do seed inicial).
4. Feature gating: como uma feature é checada (composable hasFeature? guard de
rota com meta.feature? filtro de menu?).
5. Enforcement de limites por plano: existe? (na maioria das vezes plan_limits
está semeado mas NINGUÉM lê — confirme).
6. Provisionamento de tenant: como um tenant nasce hoje (função provision_*?),
é manual (dev) ou automático? É multi-tenant por RLS ou schema-per-tenant?
Se schema-per-tenant: existe clone_tenant_schema/tenant_schema_name? O clone
copia triggers do template?
7. Fluxo de auth: onde o profile é carregado no login (carregarPerfil?), onde o
guard decide pra onde mandar o usuário (roleHomePath), e o que acontece com um
usuário logado SEM tenant.
8. Infra de e-mail: como e-mails transacionais são enviados (Resend? SMTP? edge
function?). Existe tabela de templates + algum render de {{var}}? O e-mail do
GoTrue (confirmação) funciona? Existe pg_net?
9. Infra de billing/pagamento (AsaaS/Stripe?): existe checkout de assinatura
RECORRENTE em nível de plano, ou só cobrança avulsa? Onde está o webhook?
## FASE 0.5 — DECISÕES (me apresente como perguntas; estes são os defaults que
## funcionaram bem, com o porquê):
- Provisionamento: AUTO, mas só DEPOIS de confirmar o e-mail (anti-spam: cada
signup pode clonar dezenas de tabelas).
- Funil: manter os dois caminhos (free self-service + pago via intent/comercial).
- Upgrade PRO: checkout self-service (reusar infra de pagamento existente) — mas
isso é FASE 3, deferida; no início o botão abre o canal comercial.
- Trial: o "free para sempre" substitui o trial.
- No limite: BLOQUEIA a inserção no banco (trigger) + toast amigável com CTA.
- Slug do sindicato: a pessoa escolhe (sugestão automática a partir do nome,
sanitizado), com checagem de disponibilidade ao vivo, e é IMUTÁVEL (se for
schema-per-tenant, o slug É o nome do schema → trocar órfã tudo; trave em 3
camadas: sem UI, guard no banco rejeitando UPDATE, validação na criação).
## FASE 1 — Fundação do plano gratuito
1. Migration: criar plano `gratuito` (preço 0) + plan_features (tudo ON menos o
módulo premium, ex: ordem_de_servico) + plan_limits (ex: 50 associados).
REGRA DE OURO: referencie features POR KEY via subquery, NUNCA por uuid
hardcoded (uuids de features geradas em runtime divergem entre ambientes).
Deixe o plano OCULTO na vitrine nesta fase (self-service ainda não existe).
2. Enforcement de limite GENÉRICO: uma função trigger que resolve o tenant pelo
contexto (no schema-per-tenant: pelo nome do schema = TG_TABLE_SCHEMA; no
RLS: pelo tenant_id), lê o plano ativo + plan_limits EM RUNTIME (pra mudar o
número no painel valer sem deploy), conta linhas vivas e dá RAISE com um código
parseável tipo 'PLAN_LIMIT_REACHED|<feature>|<limite>'. Trigger BEFORE INSERT
na tabela limitada. Se schema-per-tenant: coloque no template E faça backfill
nos schemas já existentes. Teste: 50 passam, 51º bloqueia; tenant pago intacto.
3. Frontend: helper que traduz o erro PLAN_LIMIT_REACHED em toast amigável com
CTA de upgrade, usado em TODOS os pontos de insert da tabela limitada. Botão
"Upgrade PRO" no topbar quando o plano do tenant for 'gratuito'.
## FASE 2 — Self-service com confirmação de e-mail
1. LIGUE a confirmação de e-mail (enable_confirmations=true no config.toml E no
dashboard do hosted).
2. ⚠️ PEGADINHA CRÍTICA #1: com confirmação ligada, o signup NÃO tem sessão. Então
TUDO que dependia de auth.uid()/JWT no signup QUEBRA em silêncio:
- inserir subscription_intents (RLS exige jwt email = email da linha) → erro.
- registrar aceite legal (LGPD) → não grava.
SOLUÇÃO: NÃO faça esses efeitos no signup. Grave a escolha (plan_key, interval,
nome/slug do sindicato, ids das versões legais aceitas) no raw_user_meta_data
do signUp, e processe TUDO no 1º login pós-confirmação, via RPCs idempotentes:
- auto_provision_free_tenant() (lê metadata, cria tenant, provisiona, vira
master, cria subscription gratuita ativa) — chamada em carregarPerfil quando
o usuário não tem tenant. Gratuito não gera intenção.
- processar_pos_signup() (aceite legal + cria a intenção SÓ pro caminho pago).
3. ⚠️ PEGADINHA CRÍTICA #2 (segurança): após o signUp, se NÃO veio sessão
(confirmação pendente), ENCERRE qualquer sessão local (signOut scope:'local')
e mostre uma tela "confirme seu e-mail". Senão, uma sessão anterior (ex: dev
testando) vaza e o push pra /login joga o usuário pro painel da sessão antiga.
A pessoa só pode logar APÓS clicar no link do e-mail.
4. ⚠️ PEGADINHA CRÍTICA #3 (blindagem): um usuário logado SEM tenant nunca pode
cair num painel quebrado. No guard, redirecione todo logado-sem-tenant (não-dev)
pra uma tela /onboarding que resolve os estados: provisionando, slug colidiu
(deixa escolher outro slug e finalizar — faça o auto_provision aceitar um
p_slug_override), conta paga aguardando ativação, sem acesso, erro (retry).
5. Signup coleta nome do sindicato + slug (sugestão + sanitização + disponibilidade
ao vivo via RPC slug_disponivel que retorna {ok, motivo}) + "seu nome".
Torne o plano gratuito visível na vitrine agora.
6. E-mail de boas-vindas: edge function (Resend) que renderiza o template, disparada
no provisionamento. Best-effort (não bloqueia o login). Destinatário derivado
do JWT, não do body.
## SAAS / EXTRAS (faça os que fizerem sentido)
- Página /saas/usuarios: 1 linha por tenant com o DONO (master) — nome, slug,
e-mail principal — via uma RPC dev-only que cruza tenants+profiles+subscriptions
(SECURITY DEFINER). Realce em verde + selo "Novo" pra cliente criado nas últimas
24h (rowClass baseado em created_at). Reaproveite essa RPC pra mostrar o e-mail
principal também nas listagens de assinaturas e tenants.
- Notificação aos devs quando nasce/muda uma assinatura (incl. trial): trigger em
subscriptions chamando a função notify_all_devs com deeplink. ⚠️ PEGADINHA #4:
se o sino de notificações é um singleton com flag "initialized", garanta que ele
RE-BUSCA ao trocar de usuário (logout+login), senão fica stale e ainda vaza
notificações entre usuários. A notificação só aparece pós-provisionamento e no
sino do DEV (não do novo usuário).
- "Esqueci meu e-mail": tela onde a pessoa informa o IDENTIFICADOR do sindicato
(slug, que ela escolheu e foi avisada ser definitivo) → o servidor acha o e-mail
do dono → mostra só uma DICA MASCARADA (jo****@gm****.com) → envia magic link
(signInWithOtp, que usa o mesmo pipeline de e-mail do GoTrue, sem depender de
Resend) → a pessoa clica e entra. O e-mail real NUNCA volta pro cliente.
- root_redirect: coluna em config + RPC pública + guard, pra escolher pra onde o
visitante não logado vai na raiz "/" (landing ou login).
- Lista de bloqueio (blacklist) de e-mails e slugs, gerida em Configurações:
tabela blacklist (kind email|slug). E-mail bloqueia o cadastro DE VERDADE via
trigger BEFORE INSERT em auth.users (não só no front); suporte a domínio inteiro
com entrada '@dominio.com'. Slug integra no slug_disponivel (motivo 'bloqueado').
## MÉTODO DE TRABALHO
- Tudo numa branch nova. Commits pequenos por assunto, mensagem clara.
- Cada migration: aplique no banco local e TESTE em transação com ROLLBACK (crie
auth.users fake + impersone via set_config('request.jwt.claims',...)) antes de
seguir. RPCs idempotentes.
- Rode o build do frontend a cada bloco pra pegar erro cedo.
- NUMERE as migrations com cuidado pra não colidir versão (quebra o db push).
- Me mostre o mapa da Fase 0 e as decisões da Fase 0.5 ANTES de codar.
## DEPLOY (no fim)
Migrations no hosted (db push) → dashboard Auth "Confirm email" ON + Site/Redirect
URLs corretas → deploy das edge functions + secret do provedor de e-mail → rebuild
do frontend → smoke test do fluxo: /lp → grátis → confirma e-mail → entra
provisionado → limite bloqueia → sino do dev → esqueci-email.
---
Esse prompt é "diretor": ele força a IA a mapear o teu outro sistema primeiro (porque as tabelas/nomes vão diferir) e
te apresentar decisões antes de codar — do jeito que fizemos aqui. As 4 pegadinhas marcadas com ⚠️ são as que mais
custaram tempo; com elas escritas, a IA evita de cara.
Quer que eu gere também uma versão curta (1 parágrafo) pra um primeiro disparo, ou uma variante específica caso o
outro sistema seja RLS puro (sem schema-per-tenant)? Aí eu ajusto os trechos de provisionamento/enforcement.