91b89b7b5d
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>
213 lines
11 KiB
Markdown
213 lines
11 KiB
Markdown
# 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
|