Files
agenciapsilmno/docs/DEPLOY_SCHEMA_PER_TENANT.md
T
Leonardo 91b89b7b5d docs: runbook de deploy da schema-per-tenant (hosted)
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>
2026-06-13 20:46:56 -03:00

11 KiB
Raw Blame History

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 (ver docs/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 em pg_db_role_setting). Um trigger em public.tenant_schemas re-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 authenticator pode estar restrita à role de serviço. Teste cedo (Fase C): rode select public.refresh_pgrst_schemas(); e cheque se os schemas tenant passam a responder via REST.
  • Fallback se o ALTER ROLE falhar 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). A fix_audit pode (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 os supabase.from(...)tenantDb().from(...) espalhados.
  • supabase/functions/_shared/tenant.ts + os webhooks/crons que passaram a rotear por schema.

Config

  • supabase/config.toml [api] schemas permanece ["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 pra OWNER 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 authenticator negado) → 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 public ainda 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 em public).
  • 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:

  1. Backup fresco obrigatório (o header do f6_3 traz o pg_dump --schema=public).
  2. Reler database-novo/manual/f6_3_ROLLBACK.md.
  3. 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.
  4. Smoke test final. A partir daqui public não tem mais as tabelas tenant — só schemas.

Rollback por fase

  • Fases AC (estrutura/funções/pgrst): aditivas. Reverter = dropar os schemas/funções novos; public intacto, 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 AF  →  (rodar dias)  →  schema-per-tenant Fase G (DROP)
                                      └─ freemium (DEPLOY_FREEMIUM_F4.md) pode entrar
                                         logo após as Fases AF (não depende do DROP)

O freemium não depende do DROP (F6.3) — depende da infra (Fases AF). 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