--- # 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||'. 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.