Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
24 KiB
Migração Schema-per-Tenant
Status: F6.0 + F6.1 concluídas e verificadas (2026-06-13). Dados dos 9 tenants migrados pros schemas, AINDA espelhados em public (nada dropado), backup em database-novo/backups/pre-F6/. Próximo: F6.2 (rewrite das funções — push dedicado), depois checkpoint de teste do app, depois F6.3 (DROP, só com OK do Leonardo). F0-F2 em main; F3+/F1b/F5/F6.0-1 no branch.
F6.0 + F6.1 — entregue (commit 003f2eb)
- F6.0 (migration 20260613000003): clone dos 9 tenants reais (schemas vazios, expostos no PostgREST via trigger F5).
- F6.1 (manual
database-novo/manual/f6_1_migrate_data.supabase_admin.sql, rodar como supabase_admin): copia dados public→schemas comsession_replication_role=replica. Tabelas com tenant_id por filtro; 3 filhas sem tenant_id (commitment_services, insurance_plan_services, recurrence_rule_services) por JOIN no pai; exclui colunas GENERATED (net_amount,margin_brl); reset de 4 sequences; ON CONFLICT DO NOTHING. - Verificado: contagens public vs schemas batem (35 patients, 37 eventos, 355 mensagens, 54 financeiro, 13 commitment_services…). notification_templates "146" = 144 seeds (16×9) + 2 tenant — esperado.
- Gotcha: colunas GENERATED não aceitam INSERT → excluir via
is_generated='NEVER'. DO block é atômico → erro no meio dá rollback total (re-rodar é seguro com ON CONFLICT).
F6.2 — PLANO (rewrite de 66 funções + split notifications) — próximo push
Decisões já tomadas: parar antes do DROP pra testar app; split notifications JUNTO na F6.
Inventário de triggers (81 attachments nas tabelas tenant em public):
- Schema-agnósticos (só NEW/OLD, sem refs a tabela): família
set_updated_at/set_*_updated_at,fn_clinical_notes_updated_at,prevent_promoting_to_system,prevent_system_group_changes,patients_validate_member_consistency,fn_agenda_regras_semanais_no_overlap→ anexar nos schemas como estão. - Schema-aware (~10, escrevem em OUTRA tabela tenant / audit / notifications) → reescrever com
set_config('search_path', TG_TABLE_SCHEMA||',public,pg_temp', true)etenant_id_for_schema(TG_TABLE_SCHEMA)onde precisam do tenant_id (audit_logs/notifications_sistema são globais):auto_create_financial_record_from_session,sync_busy_mirror_agenda_eventos,notify_on_session_status,fanout_inbound_message_to_notifications,log_audit_change,fn_sla_resolve_on_outbound,fn_clinical_note_version,fn_document_signature_timeline,fn_documents_timeline_insert,trg_fn_patient_status_history/timeline/risco,sync_legacy_email/phone_fields,agenda_cfg_sync,cancel_notifications_*,fn_notify_agenda_status_change,trg_fn_financial_records_auto_overdue. financial_records_inject_tenant→ OBSOLETO no schema (não há coluna tenant_id) — NÃO anexar.
Sub-lotes propostos (cada um com smoke test + commit):
- A ✅ DONE (commit
d58b939, migration 20260613000004):attach_agnostic_triggers(schema)recria os triggers agnósticos (8 setters updated_at + 2 prevent_*) nos 9 schemas (54 triggers/schema). Smoke: set_updated_at dispara. Wiring no clone fica pro fim da F6.2. - B ✅ DONE (commit
5741e10, manual/f6_2b_schema_aware_triggers.supabase_admin.sql — roda como supabase_admin pois trigger fns são owned por supabase_admin). 14 funcs reescritas (set_config search_path dinâmico + tenant_id_for_schema p/ audit_logs global); sync_busy_mirror cross-tenant via tenant_schema_for+EXECUTE format; financial_records_inject_tenant obsoleto (não anexado). Detach dos 14 de public + attach 22 triggers/schema (defs reais, tenant_id removido de WHEN/UPDATE OF). Smoke: sessão→realizado cria financial_record no schema + audit roteia tenant_id certo + timeline OK.- Gotchas Lote B: (1) trigger functions owned por supabase_admin → CREATE OR REPLACE só como supabase_admin (vira manual, não db.cjs). (2) Triggers reais tinham
WHEN (new.tenant_id=new.owner_id)eUPDATE OF tenant_id,...→ quebram no schema; remover tenant_id dos WHEN/colunas ao re-anexar. (3) Estratégia hybrid: detach de public pra função reescrita não rodar errada lá.
- Gotchas Lote B: (1) trigger functions owned por supabase_admin → CREATE OR REPLACE só como supabase_admin (vira manual, não db.cjs). (2) Triggers reais tinham
- C ✅ DONE (commit
bedbb9b, manual/f6_2c_notifications_split.supabase_admin.sql). DESCOBERTA: neste projeto TODAS as notifs atuais (inbound_message, session_status, system_alert, new_patient) são tenant-LOCAIS — avisos cross-tenant do SaaS vivem emglobal_notices, não em notifications. Então: notifications fica tenant-local (já nos schemas);public.notifications_sistemacriado como canal SaaS→tenant FUTURO (vazio hoje) + RLS + realtime + notify_user_sistema(). 4 notif-triggers tenant reescritos schema-aware + detach public + attach (5/schema); notify_on_intake/scheduling disparam em tabelas PUBLIC (F1b) → roteiam pro schema via tenant_schema_for+EXECUTE format; cancel_patient_pending herda search_path do chamador. Smoke: msg inbound → notif no schema, destinatário certo. Frontend notificationStore.js: load 2 fontes + merge por created_at +_origem; realtime 2 canais; markRead/archive roteiam por _origem. conversation_messages.id é bigint (gotcha no teste). - D ✅ DONE (commit
d240c66, manual/f6_2d_user_rpcs.supabase_admin.sql + 18 sites FE; build passa). 14 RPCs (list_my_signatures→F). Helper_tenant_route(p_tenant_id)valida + RETORNA schema (não seta — set_config em helper com SET search_path próprio é revertido na saída! cada RPC faz o set_config). Grupo3 RETURNS→jsonb (mark_as_paid, create_financial_record_for_session, mark_payout, create_therapist_payout). FE: p_tenant_id de activeTenantId; SETOF→jsonb transparente (nenhum consumidor indexava array). Smoke: mark_as_paid + search_global OK.- Gotchas Lote D: (1) set_config em função-helper com
SET search_pathpróprio é REVERTIDO ao retornar → helper retorna schema, RPC faz o set_config. (2) %ROWTYPE/RETURNS<tabela_tenant> quebram → RECORD/jsonb. (3) search_global é MISTO (patients/agenda no schema, patient_intake_requests em public/F1b). (4) seed_* chamados por provision ANTES do clone → no-op se schema não existe (fix de ordem no wiring). (5) can_delete_patient SQL sem SET search_path herda do chamador. - Categorias originais (ref):
- CREATE OR REPLACE, já têm p_tenant_id, RETURNS jsonb/void (sem ripple FE):
delete_commitment_full,delete_determined_commitment,seed_default_patient_groups,seed_determined_commitments(⚠️ provision_account_tenant chama seed ANTES de clone — inverter ordem no wiring do clone, senão seed escreve em public). 0 chamadas FE. - DROP+CREATE (novo p_tenant_id 1º param) + FE passa p_tenant_id, RETURNS scalar/jsonb:
cancel_recurrence_from(void,1 FE),cancelar_eventos_serie(int,0),split_recurrence_at(uuid,1),safe_delete_patient(jsonb,1),export_patient_data(jsonb,1 — toca ~10 tabelas tenant),search_global(jsonb STABLE,2),list_my_signatures(jsonb,1). - RETURNS
<tabela_tenant>/%ROWTYPE → jsonb (ripple FE: consumidores esperam row):mark_as_paid(SETOF financial_records,3 FE),create_financial_record_for_session(SETOF financial_records,6 FE — já tem p_tenant_id),mark_payout_as_paid(therapist_payouts,0),create_therapist_payout(therapist_payouts,0 — agregação financeira, testar com cuidado). - Owned mix postgres/supabase_admin → rodar migration como supabase_admin.
- Gotchas Lote D: (1) set_config em função-helper com
- E ✅ DONE (commit
02acc88, manual/f6_2e_cron_rpcs.supabase_admin.sql + 2 edge). E2 (cleanup/unstick/sync_overdue/populate_notification_queue) varrem todos os schemas via loopFROM tenant_schemas. E1 (sla_, whatsapp_heartbeat_, convert_abandoned_intake_to_lead) per-tenant via service_role: helper_tenant_schema_unchecked(SEM is_tenant_member, pq service_role não é membro) + REVOKE de anon/authenticated. first_response_stats/_runs user-facing via _tenant_route. Edge whatsapp-heartbeat/sla ajustadas (admin.rpc + p_tenant_id). Smoke OK.- Gotchas Lote E: (1) service_role NÃO é tenant_member → RPCs de cron precisam de helper sem auth-check + REVOKE de authenticated (senão um user chamaria com tenant arbitrário). (2) conversation_messages NÃO tem coluna thread_key (é computada na view) → analytics computa inline. (3) DROP+CREATE de nova assinatura: dropar AMBAS (velha+nova) p/ idempotência.
- F ✅ DONE (commit
1243a12, manual/f6_2f_anon_token_rpcs.supabase_admin.sql + 2 FE; build passa). Documentos anon resolvem tenant dedocument_share_links.tenant_id(public/F1b); agendador deagendador_configuracoes.tenant_id. document/document_signatures/access_logs/agenda no schema; share_links/agendador_* ficam public. %ROWTYPE→RECORD, RETURNS document_signatures→jsonb. sign_document_by_signature_id (paciente logado, NÃO é member): unchecked + auth por LINHA (signatario_id/email/doc do paciente). match_patient_by_phone: unchecked + REVOKE authenticated (só service). list_my_signatures: fan-out cross-schema. RPCs public-only (intake/invite/agendador_gerar_slug) SEM mudança. FE: signByPortal(tenantId,...).- Gotcha Lote F: paciente assinante NÃO é tenant_member → autorizar por LINHA (dono da assinatura), não por membership. Anon resolve tenant SEMPRE da tabela public que tem o token+tenant_id.
- G ✅ DONE (commit
ee82985, manual/f6_2g_sql_to_plpgsql.supabase_admin.sql + 3 FE; build passa). 5 funções SQL→plpgsql + p_tenant_id + _tenant_route (get_financial_summary/report, list_financial_records SETOF→jsonb, get_patient_session_counts sem filtro tenant_id). get_entity_primary_phone (interno) herda search_path. can_delete_patient/_first_response_runs já feitas em D/E. FE: p_tenant_id nas 3 RPCs financeiras.
✅✅ F6.2 COMPLETA (2026-06-13) — 66 funções migradas
Triggers (A agnósticos + B schema-aware + C notif) + RPCs (D usuário + E cron + F anon/token + G SQL→plpgsql). Tudo smoke-testado, build passa. Próximo: wiring no clone + F6.3 DROP (com OK do Leonardo).
- ✅ wiring DONE (commit
dc7826d, manual/f6_2h_clone_wiring.supabase_admin.sql): trigger AFTER INSERT em tenant_schemas (trg_attach_business_triggers) dispara os 3 attach pro schema novo → tenant novo nasce com 84 triggers. attach_agnostic agora SELF-CONTAINED (dirigido por colunas, não lê public — sobrevive ao DROP). provision_account_tenant: clone ANTES do seed. Smoke OK. - F6.3 DROP 📋 PREPARADA não-aplicada (commit
cdb9ce1, manual/f6_3_drop_public_tenant_tables.supabase_admin.sql): pré-flight assert + 2 FKs viram coluna solta (document_share_links.documento_id, whatsapp_credits_transactions.conversation_message_id) + dropa 9 views public + DROP CASCADE das 78 + limpa financial_records_inject_tenant. BLOQUEADA pelos itens em aberto abaixo.
✅ SUPERFÍCIE SaaS-ADMIN RESOLVIDA (F6.4, commit dc2363b)
RPCs saas_admin (manual/f6_4_saas_admin_rpcs.supabase_admin.sql): defaults editados no _tenant_template + fan-out pros schemas (saas_list/add/remove_default_feriado; saas_*_default_notif_template; saas_count_notif_template_overrides). Cross-tenant: saas_list_all_whatsapp_channels (fan-out, substitui v_twilio_whatsapp_overview). FE: SaasFeriadosPage/SaasNotificationTemplatesPage → RPCs; SaasWhatsappPage → supabase.schema(tenant_<slug>) (RLS permite saas_admin) p/ tenant selecionado + RPC p/ overview; getAllChannels → RPC. Varredura confirma ZERO supabase.from('<tabela_tenant>') público no FE. F6.3 DESBLOQUEADA (falta só Leonardo testar app + backup). TODOs deixados: stat-cards de feriados (cidade/estado) e incidents-7d viraram 0 (UI degrada sem crash).
(histórico) ITENS EM ABERTO antes do F6.3 DROP — RESOLVIDOS acima
Superfície SaaS-admin / cross-tenant que ainda lê public.<tabela_tenant> e quebraria no DROP:
- SaasWhatsappPage.vue + v_twilio_whatsapp_overview + twilioWhatsappService.getAllChannels() — admin cross-tenant de canais WhatsApp (notification_channels/whatsapp_connection_incidents). Reescrever fan-out por schema OU usar
public.channel_routing. - SaasNotificationTemplatesPage.vue — gerencia templates DEFAULT do sistema (tenant_id NULL). Apontar pra
_tenant_template.notification_templates(os defaults vivem lá agora). - SaasFeriadosPage.vue — gerencia feriados nacionais default. Idem
_tenant_template.feriados. - notification-webhook (Meta) — conferir fan-out/channel_routing.
Decisão de arquitetura: as páginas que editam DEFAULTS do sistema devem editar
_tenant_template(propaga a tenants novos); as views cross-tenant admin devem fan-out por schema ou usar channel_routing. Resolver, testar, então aplicar F6.3.
🟢 APP TESTÁVEL AGORA (pós-wiring, pré-DROP)
Dados nos schemas (F6.1) + 66 funções/triggers/RPCs roteiam (F6.2) + PostgREST expõe (F5) + frontend usa tenantDb (F3) + edge roteia (F4). Os dados ainda estão ESPELHADOS em public (nada dropado). Leonardo deve abrir o app no branch feat/schema-per-tenant e testar fluxos reais (agenda, financeiro, pacientes, documentos, notificações). Só após validação → F6.3 DROP.
F5 — entregue (commit 6b542cd) — PRIMEIRO teste real via HTTP do PostgREST
postgresNÃO é superuser neste stack → não consegueALTER ROLE authenticator. Quem consegue:supabase_admin(superuser, conecta com senhapostgresviapsql -U supabase_admin -h 127.0.0.1).database-novo/manual/f5_pgrst_refresh_schemas.supabase_admin.sql(aplicar como supabase_admin, fora do db.cjs):public.refresh_pgrst_schemas()(SECDEF owned supabase_admin) deriva a lista detenant_schemas, setapgrst.db_schemasin-database na role authenticator,NOTIFY pgrst reload config/schema. Expõe/retira schema SEM restart; a GUC persiste em pg_db_role_setting (sobrevive a stop/start) e SUPERSEDE o config.toml em runtime.- migration
20260613000002: trigger emtenant_schemas(AFTER INSERT/DELETE/UPDATE, statement-level) dispara o refresh → clone_tenant_template e drop_tenant_schema NÃO precisaram ser tocados. - config.toml (gitignored): baseline
public, graphql_public+ comentário; in-db config supersede. - E2E via curl: clone →
pgrst.db_schemasinclui tenant_x →GET /rest/v1/patientscomAccept-Profile: tenant_xretorna 200 (vs 406 pra schema inexistente); drop → volta 406. Tudo sem restart de container. Primeira validação real do stack F1-F5 pelo caminho HTTP do PostgREST.
Gotcha F5
- PostgREST in-database config (db-config ligada por padrão, sem
PGRST_DB_CONFIG=false):ALTER ROLE authenticator SET pgrst.db_schemas+NOTIFY pgrst, 'reload config'é a via pra schemas dinâmicos sem restart.reload schemasozinho NÃO adiciona schema novo à lista exposta — só recarrega o cache dos já expostos.
F4 — entregue (branch, commit 9b21642)
_shared/tenant.ts: helper das edge functions —adminClient()(service_role/public),tenantDbForId(admin, tenantId),schemaForTenant,listTenantSchemas(crons varrem todos),resolveTenantByChannel(webhook→tenant via channel_routing),tenantSchemaName_shared/whatsapp-hooks.tsrefatorado: hooks de tabela tenant recebemtdb; RPCs de crédito (deduct/add_whatsapp_credits) + tenant_members continuam emsupa+p_tenant_id- 23 edge functions migradas. Categorias:
- inbound (twilio/evolution): tenant_id da URL → tdb
- crons de fila (process-notification/email/sms/whatsapp-queue): varrem
listTenantSchemase drenam a fila de CADA schema — consequência direta da Q3 (filas viraram per-tenant). Modo single-tenant sebody.tenant_idvier. - crons reminders/checks (send-session-reminders, conversation-sla-check, whatsapp-heartbeat-check, convert-abandoned-intakes, sync-email-templates): loop por tenant
- routing por tenant_id (send-whatsapp-message, send-session-reminder-manual, twilio-provision, de/reactivate-channel, twilio-webhook): tenantDbForId; channel-actions sem tenant_id varrem schemas por channel_id (O(n) tenants)
- asaas-*: tenant_id do body → tdb; asaas-webhook fica global
- notification-webhook (Meta Cloud API): resolve via channel_routing por phone_number_id, fan-out por message_id quando não casa
- caller
useAgendaEventLifecycle.jspassa tenant_id pro send-session-reminder-manual (evento vive no schema) - Sem deno local → validado por grep (zero tenant_id em cadeias tdb, clients todos declarados, imports batem). Type-check real só no deploy.
⚠️ DECISÃO PENDENTE — roteamento anon-por-token (bloqueia F5/F6)
Fluxos anônimos identificam o tenant por TOKEN/SLUG, não por login, então não sabem o schema: save-intake-progress (lê patient_intake_requests por token), intake RPCs (get-intake-invite-info, submit-patient-intake), AgendadorPublicoPage+RPCs do agendador (link_slug), document share links (validate_share_token, sign_document_by_token). Opções:
- A Índice global
public_access_tokens(token_hash→tenant_id)+ triggers de sync (O(1), +1 tabela global + triggers) - B RPCs SECURITY DEFINER que varrem schemas pelo token (sem tabela nova, O(n) por request)
- C Manter as tabelas anon-facing (patient_intake_requests, patient_invites, document_share_links, agendador_configuracoes/solicitacoes) em PUBLIC com RLS por token — sidesteppa o problema; custo: essas não ganham isolamento físico (mas são as menos sensíveis, feitas pra acesso anon)
F3 — entregue (branch feat/schema-per-tenant, migration 07)
src/lib/supabase/tenantClient.js(tenantDb(),tenantSchemaName()) +src/composables/useTenantDb.jstenantStore: gettersactiveTenantSlug/activeTenantSchema;my_tenants()RPC agora devolve slug+name (migration 20260612000007)- codemod
scripts/codemod-tenant-db.py:supabase.from('<84 tabelas + 6 views tenant>')→tenantDb().from(...)em 139 arquivos (777 chamadas), removeu 173.eq('tenant_id')de cadeias tenant - 4 agentes (2 ondas) fizeram a passada manual: tenant_id fora de payloads/selects/.or/.is; onConflict ajustado (singletons →
'singleton'); realtime de tabelas tenant aponta proactiveTenantSchema; repos dropam tenant_id defensivamente de payloads de callers externos - descoberta importante: ZERO embeds cross-schema — todos os FK embeds são tenant→tenant (mesmo schema, ex.
agenda_eventos→patients,insurance_plans) ou global→global (profile_specialties→profiles). OattachProfiles/fake-embed do blueprint NÃO é necessário aqui. - gotcha:
AGENDA_EVENT_SELECT(constante de select) tinha tenant_id — selecionar coluna inexistente quebra PostgREST; varrer constantes*_SELECT, não só.from()
Pendências F3 (fora do escopo, cross-tenant/anon → tratar em F4/F6)
AgendadorPublicoPage.vue— scheduler público anon, resolve tenant porlink_slug(precisa RPC/edge de resolução slug→schema, igual channel_routing)Saas{Feriados,NotificationTemplates,DocumentTemplates,Whatsapp}Page.vue— gerenciam defaults do sistema (tenant_id NULL) ou views cross-tenant; após F6 devem mirar_tenant_templateouchannel_routing. Continuam apontando pra public (funcional até o drop da F6).
F2 — entregue (migration 20260612000006)
Os 3 únicos pontos de criação de tenant (provision_account_tenant, create_clinic_tenant, ensure_personal_tenant_for_user — este último também acionado pelo trigger de signup handle_new_user_create_personal_tenant) agora chamam clone_tenant_template() na mesma transação: clone falhou → tenant não nasce. Smoke: ensure_personal criou tenant pessoal tenant_terapeuta_pessoal com 84 tabelas + registro, 2ª chamada idempotente, drop limpou tudo. Não há fluxo de exclusão de tenant no sistema (drop_tenant_schema fica pra uso admin/manual).
F1 — entregue (migrations 20260612000001–05 em database-novo/migrations/)
tenants.slugcriado + backfill dos 9 + trigger auto-gera/imutável- Helpers:
tenant_schema_name/for,tenant_id_for_schema,tenant_schema_checked(p_tenant_id)(validais_tenant_member— substitui current_tenant_schema do blueprint) _tenant_template: 84 tabelas sem tenant_id, 6 singletons (singleton boolean PK/UQnas configs 1-linha: company_profiles, email_layout_config, conversation_autoreply_settings/bots/sla_rules, session_reminder_settings), 4 sequences locais, 94 FKs (62 intra + 32 pra public/auth), 6 views com placeholders__SCHEMA__/__TENANT_ID__em_views, seeds de sistema (whitelist 8 lookups)clone_tenant_template(uuid)→ tabelas+seqs+seeds+FKs+views+RLS (policies com tenant_id EMBUTIDO:is_tenant_member('<uuid>')+ saas_admin_full)+realtime+grants+trigger routing+registro emtenant_schemasdrop_tenant_schema(uuid)protegido;public.channel_routing(webhook inbound acha tenant do canal) sincronizada por trigger- Smoke: clone tenant_smoke_f1 → 84 tabelas/168 policies/roundtrip/routing sync/singleton rejeitando 2ª linha → drop limpo
Gotchas aprendidos na F1
postgresnão é superuser no Supabase →session_replication_roleproibido; seeds usam retry-loop de FK (rounds). Vale pro F6 (migração de dados): rodar comosupabase_adminou retry-loop.- db.cjs aplicava migration sem
ON_ERROR_STOP→ rollback silencioso reportado como sucesso. Corrigido (psqlFile agora usa-v ON_ERROR_STOP=1). - Linhas operacionais órfãs com tenant_id NULL (intakes/convites/notifs) NÃO são seeds — whitelist explícita.
- Clones F1/F2 ainda SEM triggers de negócio (F6) e fora do PostgREST (F5) —
_meta.triggers_pending=true.
Migração de multi-tenant RLS-only (tenant_id em cada tabela) para schema físico por tenant (tenant_<slug>), seguindo blueprint do projeto irmão (novo-rumo.txt na raiz), adaptado.
Artefatos
docs/F0_categorizacao.md— varredura completa: classificação das 137 tabelas, 66 funções, 6 views, FKs, edge functions, divergências.novo-rumo.txt(raiz) — blueprint original com lições do projeto irmão.
Números-chave
- 137 tabelas public → 79 tenant-scoped + 5 em decisão (infra mensageria) + 53 globais
- 66 funções afetadas (blueprint avisava: listas pré-feitas subestimam — era "29" lá, 66 aqui)
- 1 única FK global→tenant problemática:
whatsapp_credits_transactions.conversation_message_id - 0 policies de tabelas globais usando funções a refatorar
- 9 tenants (3 clínicas + 6 therapists), volumetria minúscula (<400 linhas/tabela)
Divergências vs blueprint (decisivas)
- Sem
tenants.slug— precisa criar coluna ou usar uuid no nome do schema. - Multi-membership:
profiles.tenant_id100% NULL; verdade vive emtenant_members(4 users multi-tenant).current_tenant_schema()do blueprint não funciona → frontend escolhe schema (tenantStore já temactiveTenantId), segurança via policy com tenant_id embutido por schema + RPCs recebemp_tenant_idvalidado comis_tenant_member(). - 6/9 tenants são terapeutas individuais — schema por signup; custo operacional do config.toml do PostgREST cresce com tenants.
email_layout_config.tenant_ideemail_templates_tenant.tenant_idapontam pra auth.users (legado) — mapear na migração de dados.- View
current_tenant_idé código morto (claim JWT nunca populado).
Decisões (2026-06-12)
- Q1: criar
tenants.slug→ schemastenant_<slug> - Q2: todo tenant ganha schema (clínicas e therapists)
- Q3: mensageria tenant-scoped (isolamento máximo, contra rec. global) → crons varrem tenants em loop; webhooks inbound precisam de índice global
channel_routing(channel_external_id → tenant_id) pra rotear antes de gravar - Q4: asaas tenant (staging
asaas_webhook_eventsglobal roteia)
Total final: 84 tabelas tenant-scoped, 53 globais.
Fases (tasks #1–#7 na sessão)
F0 categorização ✅ · F1 template+helpers · F2 provisionamento · F3 frontend useTenantDb · F4 edge functions · F5 PostgREST config · F6 rewrite funções + migração dados + drops (lotes, backup antes de cada um)
Relacionados: Decisões de Billing da Agenda, Supabase Local, index