- docs/F0_categorizacao.md: varredura completa (137 tabelas -> 84 tenant + 53 global, 66 funcoes, FKs, policies, edge functions) + decisoes Q1-Q4 - F1 (migrations 01-05): tenants.slug, helpers de schema, _tenant_template (84 tabelas sem tenant_id, singletons, views __SCHEMA__/__TENANT_ID__), clone_tenant_template/drop_tenant_schema, channel_routing, tenant_schemas - F2 (migration 06): provision_account_tenant/create_clinic_tenant/ ensure_personal_tenant_for_user clonam schema na mesma transacao - db.cjs: psqlFile agora usa ON_ERROR_STOP=1 (falha de migration nao passa mais como sucesso silencioso) - blueprint original em novo-rumo.txt; wiki Obsidian atualizada Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
19 KiB
F0 — Categorização para migração Schema-per-Tenant
Gerado em 2026-06-12 a partir de varredura direta no banco local (
supabase_db_agenciapsi-primesakai), grep nas edge functions (supabase/functions/) e no frontend (src/). Fonte do plano:novo-rumo.txt(blueprint do projeto irmão), adaptado às divergências deste projeto.
Sumário executivo
| Item | Quantidade |
|---|---|
Tabelas em public (BASE TABLE) |
137 |
Tenant-scoped (vão pra tenant_<x>) — decidido Q3 |
84 |
Globais (ficam em public) |
53 |
| Funções que referenciam tabelas-tenant | 66 (não 29 — o aviso do blueprint se confirmou) |
| Views que referenciam tabelas-tenant | 6 |
| FKs global→tenant problemáticas | 1 (whatsapp_credits_transactions.conversation_message_id) |
| Policies de tabelas globais usando funções a refatorar | 0 (risco de policy órfã é baixo) |
| Edge functions que tocam tabelas-tenant | ~25 de 29 |
| Tenants existentes | 9 (3 clínicas + 6 terapeutas individuais) |
| Volumetria | Baixa (maior tabela tenant: conversation_messages 355 linhas) — migração de dados é barata |
⚠️ Divergências críticas vs blueprint (novo-rumo.txt)
Estas diferenças exigem adaptação do plano — o blueprint NÃO se aplica literalmente:
D1 — Não existe tenants.slug
Colunas de tenants: id, name, created_at, kind, papel_timbrado, cpf_cnpj.
O blueprint assume slug para nomear schemas (tenant_<slug>).
Opções: (a) adicionar coluna slug (unique, imutável, sanitizada a partir de name); (b) usar tenant_ + uuid sem hífens (feio, mas sem coluna nova).
D2 — Membership multi-tenant via tenant_members (profiles.tenant_id está 100% NULL)
profiles.tenant_idexiste mas tem 0 linhas preenchidas.- Membership real:
tenant_members(15 linhas), com 4 usuários membros de mais de um tenant. - Tenant ativo é resolvido no frontend: RPC
my_tenants()→ PiniatenantStore.activeTenantId→ localStorage. Sem claim no JWT. Router troca tenant pormeta.tenantScope(clinic/personal/supervisor).
Consequência: o helper current_tenant_schema() do blueprint (baseado em profiles.tenant_id) não funciona aqui. Adaptação proposta:
- Frontend escolhe o schema diretamente:
db()=supabase.schema(tenantSchemaName(activeTenant))— já sabe o tenant ativo. - Segurança não depende da escolha do cliente: cada schema clonado ganha policies com o tenant_id embutido:
USING (public.is_tenant_member('<uuid-do-tenant>')). Usuário só lê schema de tenant onde é membro, mesmo apontando o client pra outro schema. - RPCs que precisam de "tenant atual" passam a receber
p_tenant_idexplícito, validado comis_tenant_member()antes doset_config('search_path', ...). Substituicurrent_tenant_schema()portenant_schema_checked(p_tenant_id). - Edge functions: client envia o tenant ativo (header
X-Tenant-Idou body); a function valida membership viatenant_membersantes de usar.schema().
D3 — 6 dos 9 tenants são terapeutas individuais (kind='therapist')
Schema-per-tenant aqui significa um schema por terapeuta que se cadastrar. Com 9 tenants é trivial; em escala self-serve (centenas/milhares), o array schemas do PostgREST e o catálogo do Postgres crescem linearmente. Funciona, mas é um custo operacional permanente (config.toml + restart a cada signup, a menos que automatize).
Recomendação: modelo uniforme (todo tenant ganha schema, qualquer kind) — modelo misto (clínica com schema, terapeuta em public) dobraria a complexidade de todas as funções e do frontend.
D4 — tenant_id que aponta pra auth.users (legado)
email_layout_config.tenant_id e email_templates_tenant.tenant_id têm FK pra auth.users, não pra tenants. Tratar na migração de dados (mapear user→tenant via tenant_members ou owner_id).
D5 — View current_tenant_id é código morto
SELECT current_setting('request.jwt.claim.tenant_id', true) — claim nunca populado. Remover na F6.
1. Classificação das 137 tabelas
1.1 TENANT-SCOPED — movem pra tenant_<x> (79)
Agenda (7): agenda_bloqueios, agenda_configuracoes, agenda_eventos, agenda_online_slots, agenda_regras_semanais, agenda_slots_bloqueados_semanais, agenda_slots_regras
Agendador público (2): agendador_configuracoes, agendador_solicitacoes
Asaas — cobrança de pacientes (2): asaas_customers, asaas_payments (FKs → patients/financial_records confirmam: é billing da clínica→paciente, não SaaS→tenant; ver decisão Q4)
Billing clínico (1): billing_contracts
Prontuário (3): clinical_note_templates, clinical_note_versions, clinical_notes
Compromissos (4): commitment_services (sem tenant_id — join, FK confirma), commitment_time_logs, determined_commitment_fields, determined_commitments
Cadastro da clínica (2): company_profiles, medicos
Contatos (4): contact_email_types, contact_emails, contact_phones, contact_types
Conversas/WhatsApp — conteúdo (13): conversation_assignments, conversation_autoreply_log, conversation_autoreply_settings, conversation_bot_sessions, conversation_bots, conversation_messages, conversation_notes, conversation_optout_keywords, conversation_optouts, conversation_sla_breaches, conversation_sla_rules, conversation_tags, conversation_thread_tags (dados clínicos sensíveis → LGPD favorece isolamento físico)
Documentos (6): document_access_logs, document_generated, document_share_links, document_signatures, document_templates, documents (⚠️ owned por supabase_admin — usar psql direto, gotcha conhecido)
E-mail do tenant (2): email_layout_config, email_templates_tenant (⚠️ D4)
Financeiro (4): financial_categories (sem tenant_id, 0 linhas, FK de financial_records — mover junto evita FK cross-schema), financial_exceptions, financial_records, feriados
Convênios (2): insurance_plan_services (sem tenant_id — join), insurance_plans
Notificações do tenant (4): notifications (SPLIT — ver §5), notification_preferences, notification_schedules, notification_templates
Pacientes (13): patient_contacts, patient_discounts, patient_group_patient, patient_groups, patient_intake_requests, patient_invite_attempts, patient_invites, patient_patient_tag, patient_status_history, patient_support_contacts, patient_tags, patient_timeline, patients
Precificação/pagamento (3): payment_settings, professional_pricing, services
Recorrência (3): recurrence_exceptions, recurrence_rule_services (sem tenant_id — join), recurrence_rules
Lembretes de sessão (2): session_reminder_logs, session_reminder_settings
Repasse (2): therapist_payout_records (sem tenant_id — child, FK confirma), therapist_payouts
5 tabelas tenant-scoped não têm coluna tenant_id (joins/children): commitment_services, insurance_plan_services, recurrence_rule_services, therapist_payout_records, financial_categories. A heurística "tem tenant_id" sozinha teria errado — confirmadas via FK.
1.2 Infra de mensageria (5) — ✅ DECIDIDO (Q3): TENANT-SCOPED
notification_channels, notification_queue, notification_logs, twilio_subaccount_usage, whatsapp_connection_incidents
Decisão do Leonardo (2026-06-12): isolamento físico máximo — as 5 movem pro schema do tenant (LGPD; conteúdo de mensagem nunca fica em public). Consequências assumidas:
- Os crons
process-notification-queue,process-email-queue,process-sms-queue,process-whatsapp-queue,whatsapp-heartbeat-check,conversation-sla-checkpassam a varrer tenants em loop (FOR t IN SELECT … FROM tenants/ loop no Deno com.schema()por tenant). - Webhooks inbound (twilio/evolution) resolvem o tenant pelo canal ANTES de gravar → precisa de um índice global de roteamento
public.channel_routing(channel_external_id → tenant_id) mantido por trigger no schema tenant, já que não dá pra procurar o canal em N schemas. twilio_subaccount_usage.channel_id → notification_channelsfica intra-schema (OK).- FK global→tenant continua sendo só
whatsapp_credits_transactions.conversation_message_id(vira coluna solta).
1.3 GLOBAIS — ficam em public (53)
SaaS core: tenants, tenant_members, tenant_invites, tenant_features, tenant_feature_exceptions_log, tenant_modules, profiles, profile_specialties, specialties, owner_users, saas_admins, user_settings
Planos/assinaturas: plans, plan_features, plan_prices, plan_public, plan_public_bullets, subscriptions, subscription_events, subscription_intents_legacy, subscription_intents_personal, subscription_intents_tenant, features, modules, module_features, entitlements_invalidation
Créditos/addons (billing SaaS→tenant): addon_credits, addon_products, addon_transactions, whatsapp_credit_packages, whatsapp_credit_purchases, whatsapp_credits_balance, whatsapp_credits_transactions
Plataforma: _db_migrations, asaas_webhook_events (staging de webhook — roteia pro tenant na F6), audit_logs (auditoria cross-tenant, padrão tenant_audit_log do blueprint), email_templates_global, email_layout? não — global_notices, notice_dismissals, login_carousel_slides, math_challenges, public_submission_attempts, submission_rate_limits, support_sessions, saas_doc_votos, saas_docs, saas_faq, saas_faq_itens, saas_security_config, saas_twilio_config
Dev/tracking (11): dev_auditoria_items, dev_comparison_competitor_status, dev_comparison_matrix, dev_competitor_features, dev_competitors, dev_generation_log, dev_roadmap_items, dev_roadmap_phases, dev_test_items, dev_user_credentials, dev_verificacoes_items
Tabelas com tenant_id que ficam em public (manter
.eq('tenant_id')no FE): tenant_features, tenant_feature_exceptions_log, tenant_invites, tenant_members, subscriptions, subscription_intents_, addon_credits, addon_transactions, whatsapp_credit_, audit_logs, support_sessions, profiles (+ grupo 1.2 se ficar global).
2. Funções — 66 referenciam tabelas-tenant (de 445 em public)
Por categoria (lista completa no fim):
- Triggers em tabelas tenant (18): agendador_gerar_slug, auto_create_financial_record_from_session, fanout_inbound_message_to_notifications, fn_agenda_regras_semanais_no_overlap, fn_clinical_note_version, fn_document_signature_timeline, fn_documents_timeline_insert, fn_sla_resolve_on_outbound, fn_whatsapp_low_balance_notify, notify_on_intake, notify_on_scheduling, notify_on_session_status, sync_busy_mirror_agenda_eventos, sync_legacy_email_fields, sync_legacy_phone_fields, trg_fn_patient_risco_timeline, trg_fn_patient_status_history, trg_fn_patient_status_timeline → padrão
TG_TABLE_SCHEMA+tenant_id_for_schema() - RPCs chamadas por usuário logado (~30): cancel_recurrence_from, cancelar_eventos_serie, can_delete_patient, create_financial_record_for_session, create_therapist_payout, delete_commitment_full, delete_determined_commitment, export_patient_data, get_entity_primary_phone, get_financial_report, get_financial_summary, get_patient_session_counts, issue_patient_invite, list_financial_records, list_my_signatures, mark_as_paid, mark_payout_as_paid, rotate_patient_invite_token(+v2), safe_delete_patient, search_global, seed_default_patient_groups, seed_determined_commitments, split_recurrence_at, tenant_remove_member… → padrão
p_tenant_id+is_tenant_member()+set_config('search_path')(adaptação D2) - RPCs globais/cron (~10): cleanup_notification_queue, convert_abandoned_intake_to_lead, populate_notification_queue, sync_overdue_financial_records, unstick_notification_queue, whatsapp_heartbeat_*, sla_open_breach, sla_mark_notified, first_response_stats, _first_response_runs → padrão loop
FOR t_row IN SELECT … FROM tenants - RPCs públicas/anon por token (~8): create_patient_intake_request(+v2), get_patient_intake_invite_info, get_signable_document_by_token, sign_document_by_token, sign_document_by_signature_id, validate_share_token, match_patient_by_phone, agendador_dias_disponiveis, agendador_slots_disponiveis → o token/slug identifica o tenant → resolver schema a partir do registro
- SQL puro (8): _first_response_runs, can_delete_patient, get_entity_primary_phone, get_financial_report, get_financial_summary, get_patient_session_counts, list_financial_records → converter pra plpgsql (limitação 3 do blueprint; exige DROP+CREATE)
3. Views, FKs e policies
6 views referenciam tabelas-tenant → recriar dentro do schema template: audit_log_unified (parcial — mistura audit_logs global), conversation_threads (9 usos FE + 1 edge), v_cashflow_projection, v_commitment_totals, v_patient_groups_with_counts, v_tag_patient_counts. Demais 23 views só tocam globais — intactas.
FK global→tenant (a única problemática): whatsapp_credits_transactions.conversation_message_id → conversation_messages. Decisão: converter pra coluna solta (uuid sem constraint) — billing não pode impedir DROP de schema de tenant.
FKs tenant→public (ok fisicamente, perdem embed PostgREST): patients→tenant_members (responsible/therapist_member_id), *→auth.users, financial_records→tenants (coluna some), etc. → helper attachProfiles/fake-embed no FE (limitação 1 do blueprint).
Policies: nenhuma policy de tabela global usa as 66 funções → zero recriação de policy em public. Policies das tabelas movidas morrem com elas; schemas tenant ganham policies novas no clone. Helpers RLS atuais (is_tenant_member 65 usos, is_saas_admin 177, tenant_has_feature 56, is_clinic_tenant 56) continuam válidos e são reaproveitados nas policies dos schemas tenant.
4. Edge functions — 29 no total, ~25 tocam tabelas-tenant
Mais afetadas (refs a tabelas do grupo tenant): process-notification-queue, process-email-queue, process-sms-queue, process-whatsapp-queue, send-session-reminders(+manual,+status), conversation-sla-check, evolution-whatsapp-inbound, twilio-whatsapp-inbound, send-whatsapp-message, submit-patient-intake, save-intake-progress, get-intake-invite-info, convert-abandoned-intakes, asaas-webhook, asaas-create-payment-record, asaas-cancel-payment, asaas-sync-payment, notification-webhook, sync-email-templates.
Se grupo 1.2 ficar global (Q3), os processadores de fila mantêm a maior parte do código; o refactor concentra-se em: conversation_* (12 refs), session_reminder_logs (7), notifications (2), intake (6), asaas (4), agenda_eventos (3), patients (2).
Padrão: inbound/webhook resolve tenant pelo canal/token → .schema(tenant_schema_for(tenant_id)); crons varrem tenants em loop.
5. Split de notifications (172 linhas)
Igual ao blueprint: tenant_<x>.notifications (locais) + public.notifications_sistema (cross-tenant: avisos SaaS, suporte, system_alert). Funções notify_* ganham 2 variantes; useNotifications.js (18 usos FE) mescla 2 fontes com _origem; realtime em 2 canais (canal tenant usa schema: tenant_<x>). Detalhar tipos cross-tenant vs locais na F6-Lote 1 (query por type antes do split).
6. Frontend — alvos do refactor (F3)
src/lib/supabase/client.js— cliente único; criaruseTenantDb.jsao lado.- ~100 usos
from('agenda_eventos'), 64financial_records, 45patients… (tabela completa no grep de F0). - Remover
.eq('tenant_id', …)APENAS nas tabelas que saem; manter nas globais (tenant_members, tenant_features, subscriptions, etc.). tenantStoreganhaactiveTenantSchema(computed de slug); repositories trocamsupabase.from→db().from.- Realtime: canais de tabelas tenant trocam
schema: 'public'→schema: tenantSchemaName. - Embeds
profiles!fkey(...)em tabelas tenant →attachProfiles().
7. Volumetria (migração de dados barata)
Top tenant-scoped: conversation_messages 355, notifications 172, determined_commitment_fields 117, financial_records 54, determined_commitments 47, agenda_eventos 37, patients 35. Todo o resto < 40 linhas. audit_logs (608) fica em public. Migração completa roda em segundos; ainda assim com backup por lote (regra do blueprint).
8. Decisões — ✅ respondidas pelo Leonardo em 2026-06-12
| # | Decisão | Resposta |
|---|---|---|
| Q1 | Nome do schema | Criar coluna tenants.slug (unique, imutável, gerado de name) → tenant_<slug> |
| Q2 | Quais tenants ganham schema | Todos (clínicas e therapists — modelo uniforme) |
| Q3 | Infra de mensageria (5 tabelas) | Tenant-scoped (isolamento máximo; ver §1.2 — crons em loop + índice de roteamento de canais) |
| Q4 | asaas_customers/asaas_payments | Tenant (webhook roteia via staging global asaas_webhook_events) |
Anexo — 66 funções (nome | kind | linguagem | é trigger)
_first_response_runs|sql · agendador_dias_disponiveis|plpgsql · agendador_gerar_slug|plpgsql|trg · agendador_slots_disponiveis|plpgsql · auto_create_financial_record_from_session|plpgsql|trg · can_delete_patient|sql · cancel_patient_pending_notifications|plpgsql · cancel_recurrence_from|plpgsql · cancelar_eventos_serie|plpgsql · cleanup_notification_queue|plpgsql · convert_abandoned_intake_to_lead|plpgsql · create_financial_record_for_session|plpgsql · create_patient_intake_request|plpgsql · create_patient_intake_request_v2|plpgsql · create_therapist_payout|plpgsql · delete_commitment_full|plpgsql · delete_determined_commitment|plpgsql · export_patient_data|plpgsql · fanout_inbound_message_to_notifications|plpgsql|trg · first_response_stats|plpgsql · fn_agenda_regras_semanais_no_overlap|plpgsql|trg · fn_clinical_note_version|plpgsql|trg · fn_document_signature_timeline|plpgsql|trg · fn_documents_timeline_insert|plpgsql|trg · fn_sla_resolve_on_outbound|plpgsql|trg · fn_whatsapp_low_balance_notify|plpgsql|trg · get_entity_primary_phone|sql · get_financial_report|sql · get_financial_summary|sql · get_patient_intake_invite_info|plpgsql · get_patient_session_counts|sql · get_signable_document_by_token|plpgsql · issue_patient_invite|plpgsql · list_financial_records|sql · list_my_signatures|plpgsql · mark_as_paid|plpgsql · mark_payout_as_paid|plpgsql · match_patient_by_phone|plpgsql · notify_on_intake|plpgsql|trg · notify_on_scheduling|plpgsql|trg · notify_on_session_status|plpgsql|trg · populate_notification_queue|plpgsql · rotate_patient_invite_token|plpgsql · rotate_patient_invite_token_v2|plpgsql · safe_delete_patient|plpgsql · search_global|plpgsql · seed_default_patient_groups|plpgsql · seed_determined_commitments|plpgsql · sign_document_by_signature_id|plpgsql · sign_document_by_token|plpgsql · sla_mark_notified|plpgsql · sla_open_breach|plpgsql · split_recurrence_at|plpgsql · sync_busy_mirror_agenda_eventos|plpgsql|trg · sync_legacy_email_fields|plpgsql|trg · sync_legacy_phone_fields|plpgsql|trg · sync_overdue_financial_records|plpgsql · tenant_remove_member|plpgsql · trg_fn_patient_risco_timeline|plpgsql|trg · trg_fn_patient_status_history|plpgsql|trg · trg_fn_patient_status_timeline|plpgsql|trg · unstick_notification_queue|plpgsql · validate_share_token|plpgsql · whatsapp_heartbeat_mark_notified|plpgsql · whatsapp_heartbeat_open_incident|plpgsql · whatsapp_heartbeat_resolve_open_incidents|plpgsql