Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.1 KiB
Freemium / PLG
Épico iniciado em 2026-06-13, branch feat/freemium-plg (sobre Migracao Schema-per-Tenant). Objetivo: qualquer visitante cria conta gratuita sozinho, confirma e-mail, e o ambiente do tenant é provisionado automaticamente. Plano gratuito limitado + botão "Upgrade PRO". Blueprint-diretor: novo-rumo.txt (raiz), vindo do sistema-irmão (sindicato) e adaptado a clínica.
Descoberta (Fase 0) — o que já existia
O sistema já estava ~70-85% pronto:
- Planos free existem:
clinic_free,therapist_free(+ supervisor/patient) complan_features.limitssemeado (clinic_free→clinic_calendar {max_patients:30, max_therapists:5},online_scheduling {sessions_per_month:40},reminders {reminders_per_month:50},documents.upload {max_storage_mb:500}; 14 features premium OFF). - Feature gating completo:
entitlementsStore.js(viewsv_tenant_entitlements/v_user_entitlements),FeatureGate.vue, guardmeta.feature→/upgrade(guards.js:814), badge PRO no menu. - Provisionamento schema-per-tenant:
ensure_personal_tenant/provision_account_tenant→clone_tenant_template. Setup Wizard. - Signup self-service:
/lp(pricing dinâmico dev_public_pricing) →/auth/signup(Signup.vue:219signUpinline, cria intent só no pago). - RPCs
activate_subscription_from_intent,change_subscription_plan.tenants.slug100% populado.
Gap confirmado: limites semeados mas ninguém lê/enforça. Sem confirmação de e-mail (enable_confirmations=false), sem /onboarding, signup só coleta email+senha, sem welcome email, sem os extras.
Decisões (Fase 0.5)
- Modelo do blueprint — confirmação de e-mail ON; signup grava escolha em
raw_user_meta_data+ signOut-local + tela "confirme e-mail"; provisionamento+intent viram RPCs idempotentes no 1º login (auto_provision_free_tenant(p_slug_override),processar_pos_signup); guard manda logado-sem-tenant →/onboarding. Reescreve o signup inline. - Pacientes = recurso limitado. Trigger BEFORE INSERT em
patientslê limits em runtime, resolve tenant porTG_TABLE_SCHEMA, conta linhas vivas,RAISE 'PLAN_LIMIT_REACHED|patients|<n>'. clinic_free=30, therapist_free=20. No template + backfill 9 schemas. - Slug escolhido no signup (sugestão sanitizada +
slug_disponivel(p_slug)→{ok,motivo}), imutável, trava 3 camadas. - Todos os 4 extras: /saas/usuarios +
notify_all_devs; esqueci-email (magic link por slug, dica mascarada); blacklist (email|slug); root_redirect.
Pegadinhas (do blueprint, ⚠️ caras no irmão)
- #1 Signup sem sessão (confirmação ON) → tudo com
auth.uid()quebra em silêncio. Gravar escolha em metadata, processar pós-confirmação. - #2 signOut
scope:'local'se não veio sessão — senão vaza sessão anterior e joga no painel errado. - #3 Logado-sem-tenant nunca cai em painel quebrado →
/onboardingresolve estados (provisionando, slug-colidiu, pago-aguardando, sem-acesso, erro). - #4 Sino de notificação singleton precisa re-buscar ao trocar de user (logout+login).
Divergência de infra
Blueprint pede welcome email via Resend; aqui é SMTP/Mailpit (process-email-queue). Reusar o pipeline SMTP existente (best-effort), não Resend.
Fases
- F1 ✅ DONE (2026-06-13) — therapist_free ganhou max_patients=20; trigger
enforce_patient_plan_limitem patients (lêplan_features.limitsem runtime, resolve plano viatenant_active_plan_id, conta vivos, RAISEPLAN_LIMIT_REACHED|patients|n); helpers globais + wiring + backfill 9 schemas. Front:utils/planLimit.js(toast com CTA via grupo system-alerts) nos 3 pontos de criação de paciente + botão Upgrade PRO no AppTopbar quando plano é free. Migrations:20260613000005_*+manual/freemium_f1_plan_limits.supabase_admin.sql. Testado em ROLLBACK (clinic_free bloqueia em 30, therapist_free em 20, PRO ilimitado). - F2 🟡 NÚCLEO DONE (2026-06-13) —
enable_confirmations=true(config.toml, gitignored, ativa no restart do stack); RPCsslug_disponivel/auto_provision_free_tenant/processar_pos_signup(manual/freemium_f2_provisioning.supabase_admin.sql, testados em ROLLBACK clínica+terapeuta); fix de regressãolog_audit_change(migration 20260613000006) que quebrava INSERT em tenant_members; Signup.vue reescrito (kind+nome+slug ao vivo+metadata, signOut-local + tela confirme-email); OnboardingPage.vue (provision+estados slug-colidiu/erro); guard → /onboarding; rota registrada. Build OK. Restam (polish): welcome email best-effort (infra SMTP schema-per-tenant) + apresentação do free na vitrine (public_name/preço "Grátis"/bullets — os planos já são is_visible=true mas sem nome/preço). - F3 ✅ DONE (2026-06-13) — 4 extras. DB/edge:
blacklist(tabela + trigger BEFORE INSERT em auth.users + integra slug_disponivel motivo 'bloqueado');saas_list_account_owners()(donos por tenant, dev-only) +notify_all_devs+ trigger em subscriptions;saas_app_config/get_root_redirect(); edgerecover-access(esqueci-email por slug → magic link, dica mascarada). Front: SaasUsuariosPage (/saas/usuarios, selo Novo 24h) + SaasAppConfigPage (/saas/app-config, blacklist CRUD + toggle root_redirect); esqueci-email dialog no Login; root_redirect no guard ("/" não-logado→/lp|/login, cache TTL); pegadinha #4 (notificationStore.reset no logout). Arquivos: manual/freemium_f3a/b/c + functions/recover-access. Build OK, DB testado em ROLLBACK. ⚠️ edge recover-access precisa deploy (F4). - F2 polish ✅ DONE (2026-06-13) — welcome email: edge
send-welcome-email(dono do tenant, destinatário do JWT, SMTP global/sistema com defaults Mailpit; best-effort fire-and-forget no OnboardingPage só no provision novo). Vitrine: seedplan_public+bullets dos free (migration 20260613000007); Landingpage mostra "Grátis para sempre" viaisFreePlan. ⚠️ send-welcome-email precisa deploy + envs SMTP no hosted (F4). Com isso F2 está 100%. - F4 — Deploy (hosted, dirigido pelo Leonardo).
Método: commits por assunto; cada migration testada em transação com ROLLBACK antes de aplicar; build a cada bloco front.