docs/DEPLOY_SCHEMA_PER_TENANT.md — pre-requisito do freemium. Cutover em fases: - estrategia copia-nao-move (public + schemas coexistem ate o DROP) - Risco #1 hosted: exposicao dinamica de schemas no PostgREST (ALTER ROLE authenticator) + fallback Exposed schemas no dashboard - Fase A migrations aditivas / B manual privilegiados / C pgrst dinamico (testar cedo) / D migracao de dados (janela) / E frontend+edge / F smoke+soak / G F6.3 DROP (gated, irreversivel) - rollback por fase (botao de panico = redeploy do codigo antigo ate o DROP) - freemium pode entrar apos as Fases A-F, sem depender do DROP Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Deploy — Migração Schema-per-Tenant (hosted)
Runbook de produção da migração RLS-only → schema físico por tenant (branch
feat/schema-per-tenant). Pré-requisito do freemium (verdocs/DEPLOY_FREEMIUM_F4.md). Gerado em 2026-06-13.⚠️ Esta é a migração mais delicada do projeto: envolve migração de DADOS, exposição dinâmica de schemas no PostgREST e um DROP irreversível no fim. Faça em janela de manutenção, com backup fresco, um passo de cada vez.
Estratégia de cutover (por que é seguro)
O desenho COPIA os dados (não move) de public pros schemas tenant_<slug> e
só remove o espelho de public no último passo (F6.3 DROP). Durante a transição,
os dados existem nos dois lugares → o código antigo (lê public) e o novo
(lê tenant_<slug>) funcionam simultaneamente. Isso permite:
estrutura aditiva → migra dados (copia) → sobe código novo → valida → (só então) DROP
Se algo der errado antes do DROP, é só voltar o frontend/edge pra versão antiga
(que lê public, intacto). O DROP é o único ponto de não-retorno.
⚠️ Risco hosted #1 — exposição dinâmica de schemas no PostgREST
Local: refresh_pgrst_schemas() faz ALTER ROLE authenticator SET pgrst.db_schemas=...
NOTIFY pgrst, 'reload config'(config in-database, persiste empg_db_role_setting). Um trigger empublic.tenant_schemasre-roda isso a cada clone/drop.
No Supabase hosted isso precisa ser confirmado:
- O hosted suporta a config in-DB do PostgREST, MAS a permissão de
ALTER ROLE authenticatorpode estar restrita à role de serviço. Teste cedo (Fase C): rodeselect public.refresh_pgrst_schemas();e cheque se os schemas tenant passam a responder via REST. - Fallback se o
ALTER ROLEfalhar no hosted: adicionar os schemas em Dashboard → Project Settings → API → Exposed schemas (lista). Problema: é estática — cada signup novo cria um schema que precisaria entrar na lista. Mitigação: manter o trigger in-DB (se funcionar) OU automatizar via Management API. Decidir isso ANTES de abrir pra signup self-service.
Sem exposição dos schemas tenant, o app novo recebe 404/empty nas tabelas tenant.
Inventário (branch feat/schema-per-tenant)
Migrations (aditivas — rodam como postgres / supabase db push)
Ordem natural por timestamp:
20260612000001_f1_tenants_slug.sql # tenants.slug + generate_tenant_slug + trigger
20260612000002_f1_tenant_schema_helpers.sql # tenant_schema_name, tenant_id_for_schema, ...
20260612000003_f1_tenant_template.sql # _tenant_template (78 tabelas, views, seeds)
20260612000004_f1_clone_drop_functions.sql # clone_tenant_template, drop_tenant_schema, tenant_schemas, channel_routing
20260612000005_f1_template_seed_whitelist.sql # limpa seeds órfãos
20260612000006_f2_provision_clone.sql # provision_* chamam clone
20260612000007_f3_my_tenants_slug.sql # my_tenants() retorna slug
20260613000001_f1b_keep_anon_tables_public.sql# 6 tabelas anon ficam em public
20260613000002_f5_pgrst_schemas_trigger.sql # trigger pgrst refresh em tenant_schemas
20260613000003_f6_0_clone_existing_tenants.sql# clona os tenants já existentes
20260613000004_f6_2a_attach_agnostic_triggers.sql # Lote A (triggers agnósticos)
As 3 migrations
*_freemium_*/*_fix_audit_*(000005/06/07) são do freemium — aplicar só no deploy do freemium (depois). Afix_auditpode (e deve) vir já aqui se for testar provisionamento, mas é inócua antes.
Manual supabase_admin (privilegiadas — ordem obrigatória)
f5_pgrst_refresh_schemas.supabase_admin.sql # refresh_pgrst_schemas (ALTER ROLE authenticator)
f6_2b_schema_aware_triggers.supabase_admin.sql# Lote B (14 trigger funcs schema-aware)
f6_2c_notifications_split.supabase_admin.sql # Lote C (notifications_sistema + triggers)
f6_2d_user_rpcs.supabase_admin.sql # Lote D (14 user RPCs + _tenant_route)
f6_2e_cron_rpcs.supabase_admin.sql # Lote E (cron RPCs + _tenant_schema_unchecked)
f6_2f_anon_token_rpcs.supabase_admin.sql # Lote F (anon/token RPCs)
f6_2g_sql_to_plpgsql.supabase_admin.sql # Lote G (5 SQL→plpgsql)
f6_2h_clone_wiring.supabase_admin.sql # wiring: tenants novos nascem com triggers
f6_4_saas_admin_rpcs.supabase_admin.sql # SaaS-admin RPCs (feriados/notif/whatsapp)
# DADOS:
f6_1_migrate_data.supabase_admin.sql # cutover: COPIA dados public→schemas
# DROP (último, gated):
f6_3_drop_public_tenant_tables.supabase_admin.sql # 🛑 ponto de não-retorno
Rollback do DROP documentado em database-novo/manual/f6_3_ROLLBACK.md.
Frontend / Edge (vão no rebuild + deploy)
src/lib/supabase/tenantClient.js,src/composables/useTenantDb.js,tenantStore(slug/schema getters),notificationStore(dual-source), e ossupabase.from(...)→tenantDb().from(...)espalhados.supabase/functions/_shared/tenant.ts+ os webhooks/crons que passaram a rotear por schema.
Config
supabase/config.toml [api] schemaspermanece["public","graphql_public"]— os tenant são expostos dinamicamente (não na lista). Confirmar no hosted (Risco #1).
Passo a passo
Fase 0 — Pré-flight
- Backup completo do hosted (dashboard → Database → Backups, ou
pg_dump). - Confirmar que o hosted está no baseline (branch
main/RLS) e estável. - Janela de manutenção combinada (a Fase D é cutover de dados).
- Ter a connection string de serviço em mãos (algumas etapas exigem role elevada).
Fase A — Estrutura aditiva (migrations)
Aplicar as 11 migrations 20260612*/20260613000001..000004 (e a fix_audit 000006).
Via supabase db push (com a branch linkada) ou colando no SQL Editor na ordem.
São aditivas — criam slug, helpers, _tenant_template, funções de clone, registry
tenant_schemas, e clonam os tenants existentes (000003 = f6_0). Não tocam dados.
Verificar:
select count(*) from public.tenant_schemas; -- = nº de tenants
select tenant_schema_name((select id from tenants limit 1)); -- 'tenant_<slug>'
select count(*) from information_schema.schemata where schema_name like 'tenant_%';
Fase B — Funções/triggers privilegiados (manual)
Aplicar, na ordem, via connection string de serviço (ou SQL Editor se permitir):
f6_2b → f6_2c → f6_2d → f6_2e → f6_2f → f6_2g → f6_2h → f6_4.
(São CREATE OR REPLACE / idempotentes.)
Vários fazem
ALTER FUNCTION ... OWNER TO supabase_admin. Se a role disponível no hosted não permitir, troque praOWNER TO postgres(sem perda funcional) — mesma nota do runbook do freemium.
Fase C — PostgREST dinâmico (CRÍTICO — testar cedo)
Aplicar f5_pgrst_refresh_schemas.supabase_admin.sql e disparar:
select public.refresh_pgrst_schemas(); -- seta pgrst.db_schemas + NOTIFY reload
Teste real: via REST (anon/auth key do hosted), bater numa tabela de um schema tenant
(ex.: GET /rest/v1/patients com header Accept-Profile: tenant_<slug>). Deve responder
(200/empty), não 404 "schema not exposed".
- ✅ funcionou → seguir.
- ❌ falhou (
ALTER ROLE authenticatornegado) → aplicar o fallback do Risco #1 (Exposed schemas no dashboard) antes de prosseguir, e planejar a automação por signup.
Fase D — Migração de DADOS (cutover, janela de manutenção)
Aplicar f6_1_migrate_data.supabase_admin.sql (precisa session_replication_role=replica
→ role de serviço). COPIA os dados public→schemas (idempotente, ON CONFLICT DO NOTHING).
Verificar paridade (por tabela/tenant — exemplo com patients):
-- public (origem) vs schema (destino) devem bater por tenant
select t.slug,
(select count(*) from public.patients p where p.tenant_id=t.id) as em_public,
-- ajuste o schema dinamicamente / rode por tenant:
null as em_schema
from public.tenants t order by t.slug;
-- e por schema: select count(*) from tenant_<slug>.patients;
Repetir o spot-check nas tabelas de maior volume (conversation_messages, financial_records, agenda_eventos).
Fase E — Frontend + Edge (sobe o código novo)
- Deploy das edge functions alteradas (
supabase functions deploy <nome>pras que mudaram: webhooks twilio/evolution inbound, crons de fila,_shared/tenant.tsé embutido). - Rebuild + publish do frontend da branch (agora
tenantDb().from(...)lê os schemas). - A partir daqui o app lê/escreve nos schemas tenant. Como os dados foram copiados na
Fase D e
publicainda existe, nada quebra mesmo se algum ponto antigo escapar.
Fase F — Smoke test (app no modelo novo)
- Login em 2-3 tenants distintos → agenda, pacientes, financeiro, conversas carregam.
- Criar/editar registros → conferir que gravam em
tenant_<slug>(não empublic). - Notificações (sino) — dual-source (tenant +
notifications_sistema). - Webhook inbound (twilio/evolution) grava no schema certo (roteamento por canal).
- Crons (fila de notificação/email) varrem os tenants.
- Provisionar um tenant NOVO de teste → nasce com schema + triggers (wiring f6_2h).
- Deixar rodando alguns dias com os dados ainda espelhados em public (rede de segurança).
Fase G — F6.3 DROP (🛑 PONTO DE NÃO-RETORNO)
Só depois de F validada por dias + sem incidentes. Sequência:
- Backup fresco obrigatório (o header do f6_3 traz o
pg_dump --schema=public). - Reler
database-novo/manual/f6_3_ROLLBACK.md. - Aplicar
f6_3_drop_public_tenant_tables.supabase_admin.sql(role de serviço): pré-flight asserts → 2 FK→coluna solta → drop 9 views → DROP CASCADE 78 tabelas public. - Smoke test final. A partir daqui
publicnão tem mais as tabelas tenant — só schemas.
Rollback por fase
- Fases A–C (estrutura/funções/pgrst): aditivas. Reverter = dropar os schemas/funções
novos;
publicintacto, app antigo segue. Sem perda. - Fase D (dados): só copiou; reverter = ignorar/limpar schemas.
publicé a verdade. - Fase E (código): rollback = redeploy do frontend/edge da versão antiga (lê public). Esse é o botão de pânico até o DROP.
- Fase G (DROP): irreversível sem restore. Rollback = restaurar do backup (ver
f6_3_ROLLBACK.md). Por isso só após dias de validação.
Ordem geral dos dois épicos
schema-per-tenant Fases A–F → (rodar dias) → schema-per-tenant Fase G (DROP)
└─ freemium (DEPLOY_FREEMIUM_F4.md) pode entrar
logo após as Fases A–F (não depende do DROP)
O freemium não depende do DROP (F6.3) — depende da infra (Fases A–F). Dá pra subir o freemium assim que o schema-per-tenant estiver validado no hosted, mantendo o espelho em public como rede de segurança, e fazer o DROP com calma depois.
Checklist
- Fase 0: backup + janela + baseline confirmado
- Fase A: 11 migrations aplicadas + verificação
- Fase B: 9 manual (b→4) na ordem
- Fase C: pgrst dinâmico testado via REST (ou fallback decidido)
- Fase D: f6_1 + paridade de contagens conferida
- Fase E: edges + frontend novos publicados
- Fase F: smoke test + dias de soak
- Fase G: backup fresco → DROP → smoke final