# 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_` 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_`) 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:** ```sql select count(*) from public.tenant_schemas; -- = nº de tenants select tenant_schema_name((select id from tenants limit 1)); -- 'tenant_' 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: ```sql 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_`). 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`): ```sql -- 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_.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 ` 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_` (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 A–C** (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 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