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>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# 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:**
|
||||
```sql
|
||||
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:
|
||||
```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_<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`):
|
||||
```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_<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 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
|
||||
Reference in New Issue
Block a user