a73b82fa86
- 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>
150 lines
10 KiB
Plaintext
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. |