@luanpdd/kit-mcp 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +648 -648
- package/gates/dept-cycle-prevention.md +179 -0
- package/gates/multi-tenant-rls-coverage.md +102 -0
- package/gates/service-role-not-in-user-facing.md +113 -0
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +52 -52
- package/kit/agents/advisor-researcher.md +106 -106
- package/kit/agents/assumptions-analyzer.md +107 -107
- package/kit/agents/audit-log-implementer.md +175 -0
- package/kit/agents/b2b-saas-architect.md +156 -0
- package/kit/agents/codebase-mapper.md +768 -768
- package/kit/agents/crm-pipeline-implementer.md +150 -0
- package/kit/agents/debugger.md +772 -772
- package/kit/agents/evolution-go-integrator.md +179 -0
- package/kit/agents/example-reviewer.md +21 -21
- package/kit/agents/executor.md +523 -523
- package/kit/agents/integration-checker.md +200 -200
- package/kit/agents/invite-flow-implementer.md +137 -0
- package/kit/agents/lgpd-compliance-auditor.md +206 -0
- package/kit/agents/multi-tenant-isolation-auditor.md +243 -0
- package/kit/agents/multi-tenant-rls-writer.md +262 -0
- package/kit/agents/nyquist-auditor.md +178 -178
- package/kit/agents/org-onboarding-implementer.md +202 -0
- package/kit/agents/phase-researcher.md +696 -696
- package/kit/agents/plan-checker.md +272 -272
- package/kit/agents/planner.md +891 -891
- package/kit/agents/project-researcher.md +652 -652
- package/kit/agents/research-synthesizer.md +245 -245
- package/kit/agents/roadmapper.md +677 -677
- package/kit/agents/super-admin-implementer.md +182 -0
- package/kit/agents/ui-auditor.md +437 -437
- package/kit/agents/ui-checker.md +302 -302
- package/kit/agents/ui-researcher.md +355 -355
- package/kit/agents/user-profiler.md +175 -175
- package/kit/agents/verifier.md +728 -728
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/multi-tenant.md +163 -0
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +30 -3
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/_shared-multi-tenant/glossary.md +186 -0
- package/kit/skills/audit-log-multi-tenant/SKILL.md +334 -0
- package/kit/skills/b2b-saas-architecture/SKILL.md +300 -0
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +326 -0
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -0
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -0
- package/kit/skills/member-invite-flow/SKILL.md +305 -0
- package/kit/skills/member-management-react-shadcn/SKILL.md +328 -0
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +312 -0
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +338 -0
- package/kit/skills/org-onboarding-flow/SKILL.md +257 -0
- package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -0
- package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -0
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +301 -0
- package/kit/skills/super-admin-platform-pattern/SKILL.md +322 -0
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -0
- package/package.json +63 -63
- package/src/core/kit.js +216 -216
- package/src/core/reflect.js +247 -247
- package/src/core/reverse-sync.js +372 -372
- package/src/core/sync.js +418 -418
- package/src/core/watch.js +121 -121
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-log-multi-tenant
|
|
3
|
+
description: Use ao implementar audit log em B2B SaaS multi-tenant Supabase — tabela append-only (REVOKE DELETE/UPDATE), 7 event types canônicos, retention pg_cron 3 tiers (30d/90d/365d), legal_hold flag para LGPD erasure, PII sanitization (hash actor_email), tenant_id obrigatório indexed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Audit Log Multi-Tenant — Compliance + Forensics
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill ao implementar audit log em B2B SaaS multi-tenant. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "audit log multi-tenant", "audit trail compliance"
|
|
13
|
+
- "append-only table Postgres", "REVOKE DELETE UPDATE"
|
|
14
|
+
- "retention pg_cron tiers"
|
|
15
|
+
- "legal hold LGPD erasure"
|
|
16
|
+
- "PII sanitization audit"
|
|
17
|
+
- "event taxonomy canônica"
|
|
18
|
+
|
|
19
|
+
Esta skill é consumida pelo agent `audit-log-implementer` (Phase 109) que materializa migration + retention scheduler.
|
|
20
|
+
|
|
21
|
+
## Regras absolutas
|
|
22
|
+
|
|
23
|
+
**REGRA #1 (append-only):** Tabela `audit_logs` é **append-only** via `REVOKE DELETE, UPDATE ON public.audit_logs FROM authenticated`. Apenas service_role pode mutar (via partition swap, raramente). Comprometimento de admin não consegue apagar evidências.
|
|
24
|
+
|
|
25
|
+
**REGRA #2 (tenant_id obrigatório indexed first):** Toda row em `audit_logs` tem `tenant_id` (= `org_id`) **NOT NULL** + index composite `(tenant_id, created_at desc)` como **primeira** coluna. Sem isso, queries "todos eventos da org X" viram table scan.
|
|
26
|
+
|
|
27
|
+
**REGRA #3 (legal_hold flag):** Coluna `legal_hold boolean default false`. Quando user da org X faz DSR LGPD de erasure, marcar `legal_hold = true` em todas suas rows pendentes — bloqueia delete pelo retention scheduler até DSR processada.
|
|
28
|
+
|
|
29
|
+
**REGRA #4 (PII sanitization):** `actor_email` e `target_email` armazenados como **SHA-256 hash** (não raw). Para investigação, forensic mode rehasha email candidato e busca match. PII em log = LGPD violation.
|
|
30
|
+
|
|
31
|
+
**REGRA #5 (retention 3 tiers):** Default retention via pg_cron:
|
|
32
|
+
- **Free tier**: 30 dias
|
|
33
|
+
- **Pro tier**: 90 dias
|
|
34
|
+
- **Enterprise tier**: 365 dias
|
|
35
|
+
- **Sempre respeitando legal_hold = true** (skip rows com legal hold)
|
|
36
|
+
|
|
37
|
+
**REGRA #6 (event taxonomy mínima — 7 events):** `login`, `member_invited`, `role_changed`, `data_exported`, `member_removed`, `settings_changed`, `super_admin_action`. Custom events permitidos via campo `event_type text` mas estes 7 são obrigatórios em qualquer app B2B.
|
|
38
|
+
|
|
39
|
+
## Patterns canônicos
|
|
40
|
+
|
|
41
|
+
### Tabela `audit_logs` — DDL completo
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
-- Tabela append-only, particionada por tenant_id (LIST partitioning) se >50k rows/tenant
|
|
45
|
+
create table public.audit_logs (
|
|
46
|
+
id uuid not null default gen_random_uuid(),
|
|
47
|
+
tenant_id uuid not null references public.organizations(id) on delete cascade,
|
|
48
|
+
event_type text not null check (event_type ~ '^[a-z_]+$'),
|
|
49
|
+
actor_id uuid references auth.users(id) on delete set null, -- NULL após erasure
|
|
50
|
+
actor_email_hash text, -- SHA-256, REGRA #4
|
|
51
|
+
target_id uuid, -- ID do recurso afetado (lead, member, etc.)
|
|
52
|
+
target_type text, -- 'lead', 'member', 'org', etc.
|
|
53
|
+
target_email_hash text, -- SHA-256 do email se aplicável
|
|
54
|
+
payload jsonb, -- detalhes do evento (campos changed, etc.)
|
|
55
|
+
ip_address inet, -- IP de origem (opcional)
|
|
56
|
+
user_agent text, -- UA (opcional)
|
|
57
|
+
legal_hold boolean not null default false, -- REGRA #3
|
|
58
|
+
created_at timestamptz not null default now(),
|
|
59
|
+
primary key (id, tenant_id), -- composite para particionamento
|
|
60
|
+
-- REGRA #2: index composite tenant_id first
|
|
61
|
+
constraint event_type_canonical check (
|
|
62
|
+
event_type in (
|
|
63
|
+
'login', 'member_invited', 'role_changed', 'data_exported',
|
|
64
|
+
'member_removed', 'settings_changed', 'super_admin_action'
|
|
65
|
+
) or event_type ~ '^custom_[a-z_]+$' -- custom events com prefix
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
-- Index composite tenant_id first (REGRA #2)
|
|
70
|
+
create index audit_logs_tenant_created_idx
|
|
71
|
+
on public.audit_logs (tenant_id, created_at desc);
|
|
72
|
+
|
|
73
|
+
-- Index para busca por actor (compliance investigation)
|
|
74
|
+
create index audit_logs_actor_id_idx
|
|
75
|
+
on public.audit_logs (actor_id, created_at desc) where actor_id is not null;
|
|
76
|
+
|
|
77
|
+
-- Index para legal hold (rotina retention precisa filtrar)
|
|
78
|
+
create index audit_logs_legal_hold_idx
|
|
79
|
+
on public.audit_logs (legal_hold, created_at) where legal_hold = false;
|
|
80
|
+
|
|
81
|
+
-- REGRA #1: append-only — REVOKE DELETE e UPDATE
|
|
82
|
+
alter table public.audit_logs enable row level security;
|
|
83
|
+
|
|
84
|
+
revoke delete on public.audit_logs from authenticated, anon;
|
|
85
|
+
revoke update on public.audit_logs from authenticated, anon;
|
|
86
|
+
-- service_role bypassa RLS — partition swap futuro é o único delete legítimo
|
|
87
|
+
|
|
88
|
+
-- POLICY SELECT: members da org com permission view:audit_logs
|
|
89
|
+
create policy "audit_logs_select_with_permission"
|
|
90
|
+
on public.audit_logs
|
|
91
|
+
for select
|
|
92
|
+
to authenticated
|
|
93
|
+
using (
|
|
94
|
+
private.has_permission('view', 'audit_logs', tenant_id)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
-- POLICY INSERT: qualquer authenticated pode inserir (helper function escreve via SECURITY DEFINER)
|
|
98
|
+
create policy "audit_logs_insert_authenticated"
|
|
99
|
+
on public.audit_logs
|
|
100
|
+
for insert
|
|
101
|
+
to authenticated
|
|
102
|
+
with check (
|
|
103
|
+
tenant_id is not null
|
|
104
|
+
and event_type is not null
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
-- POLICY super_admin bypass (PERMISSIVE)
|
|
108
|
+
create policy "audit_logs_super_admin_bypass"
|
|
109
|
+
on public.audit_logs
|
|
110
|
+
as permissive
|
|
111
|
+
for select
|
|
112
|
+
to authenticated
|
|
113
|
+
using (private.is_super_admin());
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Helper function — emit audit event
|
|
117
|
+
|
|
118
|
+
```sql
|
|
119
|
+
-- SECURITY DEFINER porque precisa hash email mesmo se user não tiver INSERT direto
|
|
120
|
+
create or replace function private.audit_log(
|
|
121
|
+
p_event_type text,
|
|
122
|
+
p_tenant_id uuid,
|
|
123
|
+
p_target_id uuid default null,
|
|
124
|
+
p_target_type text default null,
|
|
125
|
+
p_target_email text default null,
|
|
126
|
+
p_payload jsonb default null
|
|
127
|
+
)
|
|
128
|
+
returns void
|
|
129
|
+
language plpgsql
|
|
130
|
+
security definer
|
|
131
|
+
set search_path = ''
|
|
132
|
+
as $$
|
|
133
|
+
declare
|
|
134
|
+
v_actor_id uuid;
|
|
135
|
+
v_actor_email text;
|
|
136
|
+
begin
|
|
137
|
+
v_actor_id := (select auth.uid());
|
|
138
|
+
select email into v_actor_email from auth.users where id = v_actor_id;
|
|
139
|
+
|
|
140
|
+
insert into public.audit_logs (
|
|
141
|
+
tenant_id,
|
|
142
|
+
event_type,
|
|
143
|
+
actor_id,
|
|
144
|
+
actor_email_hash,
|
|
145
|
+
target_id,
|
|
146
|
+
target_type,
|
|
147
|
+
target_email_hash,
|
|
148
|
+
payload
|
|
149
|
+
)
|
|
150
|
+
values (
|
|
151
|
+
p_tenant_id,
|
|
152
|
+
p_event_type,
|
|
153
|
+
v_actor_id,
|
|
154
|
+
case when v_actor_email is not null then encode(digest(v_actor_email, 'sha256'), 'hex') end,
|
|
155
|
+
p_target_id,
|
|
156
|
+
p_target_type,
|
|
157
|
+
case when p_target_email is not null then encode(digest(p_target_email, 'sha256'), 'hex') end,
|
|
158
|
+
p_payload
|
|
159
|
+
);
|
|
160
|
+
end;
|
|
161
|
+
$$;
|
|
162
|
+
|
|
163
|
+
-- Permitir authenticated chamar
|
|
164
|
+
grant execute on function private.audit_log(text, uuid, uuid, text, text, jsonb) to authenticated;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Retention via pg_cron — 3 tiers
|
|
168
|
+
|
|
169
|
+
```sql
|
|
170
|
+
-- Schedule diário 03:00 UTC — apaga rows velhas respeitando legal_hold
|
|
171
|
+
select cron.schedule(
|
|
172
|
+
'audit-log-retention',
|
|
173
|
+
'0 3 * * *',
|
|
174
|
+
$$
|
|
175
|
+
-- Free tier orgs: 30 dias
|
|
176
|
+
delete from public.audit_logs al
|
|
177
|
+
using public.organizations o
|
|
178
|
+
where al.tenant_id = o.id
|
|
179
|
+
and o.plan = 'free'
|
|
180
|
+
and al.created_at < now() - interval '30 days'
|
|
181
|
+
and al.legal_hold = false;
|
|
182
|
+
|
|
183
|
+
-- Pro tier orgs: 90 dias
|
|
184
|
+
delete from public.audit_logs al
|
|
185
|
+
using public.organizations o
|
|
186
|
+
where al.tenant_id = o.id
|
|
187
|
+
and o.plan = 'pro'
|
|
188
|
+
and al.created_at < now() - interval '90 days'
|
|
189
|
+
and al.legal_hold = false;
|
|
190
|
+
|
|
191
|
+
-- Enterprise tier orgs: 365 dias
|
|
192
|
+
delete from public.audit_logs al
|
|
193
|
+
using public.organizations o
|
|
194
|
+
where al.tenant_id = o.id
|
|
195
|
+
and o.plan = 'enterprise'
|
|
196
|
+
and al.created_at < now() - interval '365 days'
|
|
197
|
+
and al.legal_hold = false;
|
|
198
|
+
$$
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Nota:** o DELETE acima é executado pelo pg_cron com role `postgres` (bypassa RLS). NÃO é `authenticated` — REGRA #1 não é violada.
|
|
203
|
+
|
|
204
|
+
### Emit eventos canônicos — exemplos
|
|
205
|
+
|
|
206
|
+
```sql
|
|
207
|
+
-- Login (após auth callback)
|
|
208
|
+
select private.audit_log(
|
|
209
|
+
p_event_type := 'login',
|
|
210
|
+
p_tenant_id := <org_id>,
|
|
211
|
+
p_payload := jsonb_build_object('ip', '<ip>', 'user_agent', '<ua>')
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
-- Member invited
|
|
215
|
+
select private.audit_log(
|
|
216
|
+
p_event_type := 'member_invited',
|
|
217
|
+
p_tenant_id := <org_id>,
|
|
218
|
+
p_target_id := <invited_user_id>,
|
|
219
|
+
p_target_email := <invited_email>,
|
|
220
|
+
p_payload := jsonb_build_object('role', 'admin', 'invited_by', '<actor_email_hash>')
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
-- Role changed
|
|
224
|
+
select private.audit_log(
|
|
225
|
+
p_event_type := 'role_changed',
|
|
226
|
+
p_tenant_id := <org_id>,
|
|
227
|
+
p_target_id := <member_user_id>,
|
|
228
|
+
p_payload := jsonb_build_object('from_role', 'member', 'to_role', 'admin')
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
-- super_admin action (chamado pelo trigger gerado por multi-tenant-rls-writer)
|
|
232
|
+
select private.audit_log(
|
|
233
|
+
p_event_type := 'super_admin_action',
|
|
234
|
+
p_tenant_id := <target_org_id>,
|
|
235
|
+
p_payload := jsonb_build_object('table', 'leads', 'op', 'DELETE', 'reason', 'cleanup')
|
|
236
|
+
);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Query forensics — investigar incident
|
|
240
|
+
|
|
241
|
+
```sql
|
|
242
|
+
-- "Quem deletou todos os leads da org X em 2026-04-15?"
|
|
243
|
+
select
|
|
244
|
+
al.created_at,
|
|
245
|
+
al.event_type,
|
|
246
|
+
al.actor_email_hash, -- hash, mas pode rehasher candidates
|
|
247
|
+
al.payload
|
|
248
|
+
from public.audit_logs al
|
|
249
|
+
where al.tenant_id = '<org_id>'
|
|
250
|
+
and al.created_at::date = '2026-04-15'
|
|
251
|
+
and al.event_type in ('super_admin_action', 'data_exported')
|
|
252
|
+
and al.payload->>'table' = 'leads'
|
|
253
|
+
order by al.created_at;
|
|
254
|
+
|
|
255
|
+
-- Match actor por email (forensics) — calcular hash do email candidato
|
|
256
|
+
select * from public.audit_logs
|
|
257
|
+
where actor_email_hash = encode(digest('admin@acme.com', 'sha256'), 'hex')
|
|
258
|
+
and tenant_id = '<org_id>'
|
|
259
|
+
order by created_at desc
|
|
260
|
+
limit 100;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Anti-patterns
|
|
264
|
+
|
|
265
|
+
### Anti-pattern 1: Tabela audit_logs sem REVOKE
|
|
266
|
+
|
|
267
|
+
**Errado:**
|
|
268
|
+
```sql
|
|
269
|
+
create table public.audit_logs (...);
|
|
270
|
+
alter table public.audit_logs enable row level security;
|
|
271
|
+
-- Sem REVOKE — admin pode delete via UPDATE policy
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Por quê:** atacante ou admin compromised pode `DELETE FROM audit_logs WHERE event_type = 'super_admin_action'` e apagar evidências de acesso indevido.
|
|
275
|
+
|
|
276
|
+
**Certo:** `REVOKE DELETE, UPDATE ON public.audit_logs FROM authenticated, anon;`
|
|
277
|
+
|
|
278
|
+
### Anti-pattern 2: actor_email/target_email em raw
|
|
279
|
+
|
|
280
|
+
**Errado:**
|
|
281
|
+
```sql
|
|
282
|
+
create table public.audit_logs (
|
|
283
|
+
...
|
|
284
|
+
actor_email text, -- raw email
|
|
285
|
+
target_email text -- raw email
|
|
286
|
+
);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Por quê:** PII em audit log = LGPD violation. DSR de erasure de user X torna-se complexo (precisa anonimizar todas as rows com email do user).
|
|
290
|
+
|
|
291
|
+
**Certo:** SHA-256 hash. Para forensics, rehasher email candidato e buscar match.
|
|
292
|
+
|
|
293
|
+
### Anti-pattern 3: Retention sem respeitar legal_hold
|
|
294
|
+
|
|
295
|
+
**Errado:**
|
|
296
|
+
```sql
|
|
297
|
+
delete from public.audit_logs where created_at < now() - interval '30 days';
|
|
298
|
+
-- Sem filter legal_hold
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Por quê:** apaga evidências necessárias para responder DSR LGPD em curso. Legal violation.
|
|
302
|
+
|
|
303
|
+
**Certo:** `... and legal_hold = false`. Quando DSR processada, marcar `legal_hold = false` permite next retention run.
|
|
304
|
+
|
|
305
|
+
### Anti-pattern 4: tenant_id ausente ou nullable
|
|
306
|
+
|
|
307
|
+
**Errado:**
|
|
308
|
+
```sql
|
|
309
|
+
tenant_id uuid -- nullable
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Por quê:** queries "todos eventos da org X" precisam filtrar por tenant_id. NULL quebra filter (`tenant_id = $1` exclui NULLs). Index composite (tenant_id, created_at) menos eficaz com NULLs.
|
|
313
|
+
|
|
314
|
+
**Certo:** `tenant_id uuid not null references public.organizations(id) on delete cascade`. NOT NULL + FK garante consistência.
|
|
315
|
+
|
|
316
|
+
### Anti-pattern 5: Single audit_logs sem partitioning para org grande
|
|
317
|
+
|
|
318
|
+
**Errado:**
|
|
319
|
+
```sql
|
|
320
|
+
-- Org enterprise com 10M events/ano em tabela única
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Por quê:** vacuum lento, queries lentas, retention DELETE bloqueia outras escritas.
|
|
324
|
+
|
|
325
|
+
**Certo:** LIST partitioning por `tenant_id` (ver [`multi-tenant-performance-scaling`](../multi-tenant-performance-scaling/SKILL.md)). Cada org = partição própria. Retention vira `DROP TABLE <partition>` (instantâneo).
|
|
326
|
+
|
|
327
|
+
## Ver também
|
|
328
|
+
|
|
329
|
+
- [supabase-cron-queues](../supabase-cron-queues/SKILL.md) — pg_cron pattern usado para retention scheduler (cross-suite)
|
|
330
|
+
- [multi-tenant-performance-scaling](../multi-tenant-performance-scaling/SKILL.md) — partitioning por org_id (REGRA #5)
|
|
331
|
+
- [multi-tenant-rls-hierarchy](../multi-tenant-rls-hierarchy/SKILL.md) — `private.is_super_admin` + super_admin trigger pattern
|
|
332
|
+
- [lgpd-multi-tenant-compliance](../lgpd-multi-tenant-compliance/SKILL.md) — Phase 114, integração com legal_hold
|
|
333
|
+
- [super-admin-platform-pattern](../super-admin-platform-pattern/SKILL.md) — Phase 111, `super_admin_action` event obrigatório
|
|
334
|
+
- [_shared-multi-tenant/glossary.md](../_shared-multi-tenant/glossary.md) — termos `audit log`, `event taxonomy`, `legal hold`, `PII sanitization`
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: b2b-saas-architecture
|
|
3
|
+
description: Use ao desenhar app B2B multi-tenant (org→department→leader→collaborator) com Supabase + React — Single Schema + org_id + RLS é default; JWT minimal (super_admin: bool); 7 tabelas canônicas; slug imutável.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# B2B SaaS Multi-Tenant — Arquitetura Canônica
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill ao desenhar arquitetura de app B2B SaaS multi-tenant em Supabase. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "B2B SaaS multi-tenant", "arquitetura multi-tenant", "isolation strategy"
|
|
13
|
+
- "single schema vs schema-per-tenant"
|
|
14
|
+
- "schema canônico organizations", "departments table", "members"
|
|
15
|
+
- "JWT claims multi-tenant", "app_metadata orgs"
|
|
16
|
+
- "slug org imutável", "tenant routing"
|
|
17
|
+
|
|
18
|
+
Esta skill define o **schema canônico** que `multi-tenant-rls-writer` (Phase 108), `org-onboarding-implementer` (Phase 107), `super-admin-implementer` (Phase 111), `audit-log-implementer` (Phase 109), e demais agents da suíte v1.21 consomem como entrada.
|
|
19
|
+
|
|
20
|
+
## Regras absolutas
|
|
21
|
+
|
|
22
|
+
**REGRA #1 (estratégia default):** **Single Schema + `org_id` + RLS** é o caminho canônico para 90% dos B2B SaaS. Schema-per-tenant é justificado apenas em compliance extremo (saúde/jurídico com auditoria isolacional). Database-per-tenant é inviável economicamente fora de contratos enterprise.
|
|
23
|
+
|
|
24
|
+
**REGRA #2 (JWT minimal):** **APENAS** `super_admin: bool` em `app_metadata`. Lista de orgs no JWT é anti-pattern — bloat linear no token + stale de 1h após mudança de role. O banco (helper functions PG) é fonte de verdade.
|
|
25
|
+
|
|
26
|
+
**REGRA #3 (slug imutável):** `organizations.slug` é **append-only** após criação. Mutação requer tabela `slug_history` + redirect 301 em rotas afetadas. Quebra silenciosa de bookmarks/webhooks/OAuth callbacks é o pior bug que cliente B2B encontra.
|
|
27
|
+
|
|
28
|
+
**REGRA #4 (super_admin via service_role):** `app_metadata.super_admin = true` é setado **APENAS** via `auth.admin.updateUserById()` (service role). Cliente NUNCA consegue mutá-lo (≠ `user_metadata` que é editável).
|
|
29
|
+
|
|
30
|
+
**REGRA #5 (FKs com CASCADE explícito):** Todas as FKs têm `ON DELETE` explícito (CASCADE para entidades dependentes da org, RESTRICT para evitar deleção acidental). Sem default — força decisão consciente.
|
|
31
|
+
|
|
32
|
+
## Patterns canônicos
|
|
33
|
+
|
|
34
|
+
### Estratégia de isolation — tabela comparativa
|
|
35
|
+
|
|
36
|
+
| Estratégia | Isolation | Custo ops | Compliance | Quando usar |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| **Single Schema + `org_id` + RLS** ⭐ | Lógico (RLS) | Baixo | Padrão B2B SaaS | **DEFAULT 90% dos casos** — Stripe, Linear, Vercel, Notion |
|
|
39
|
+
| Schema-per-tenant | Físico (PG schemas) | Médio (N migrations) | Compliance auditável | Saúde/jurídico/governo com requisito explícito de isolamento |
|
|
40
|
+
| Database-per-tenant | Físico (DB separadas) | Alto (N projects Supabase) | Compliance extremo | Apenas contratos enterprise com SLA de isolamento físico |
|
|
41
|
+
|
|
42
|
+
**Recomendação:** comece sempre com Single Schema. Migração para schema-per-tenant é viável (script de fan-out por org_id). Migração reversa não é.
|
|
43
|
+
|
|
44
|
+
### Schema canônico — 7 tabelas (DDL completo)
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
-- Ordem de criação respeita dependências FK
|
|
48
|
+
|
|
49
|
+
-- 1. organizations (root tenant)
|
|
50
|
+
create table public.organizations (
|
|
51
|
+
id uuid primary key default gen_random_uuid(),
|
|
52
|
+
name text not null,
|
|
53
|
+
slug text unique not null check (slug ~ '^[a-z0-9-]+$' and length(slug) between 2 and 60),
|
|
54
|
+
owner_id uuid not null references auth.users(id) on delete restrict,
|
|
55
|
+
plan text not null default 'free' check (plan in ('free', 'pro', 'enterprise')),
|
|
56
|
+
status text not null default 'active' check (status in ('active', 'suspended', 'archived')),
|
|
57
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
58
|
+
created_at timestamptz not null default now(),
|
|
59
|
+
updated_at timestamptz not null default now()
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- 2. departments (sub-tenant opcional)
|
|
63
|
+
create table public.departments (
|
|
64
|
+
id uuid primary key default gen_random_uuid(),
|
|
65
|
+
org_id uuid not null references public.organizations(id) on delete cascade,
|
|
66
|
+
parent_id uuid references public.departments(id) on delete set null,
|
|
67
|
+
name text not null,
|
|
68
|
+
slug text not null check (slug ~ '^[a-z0-9-]+$'),
|
|
69
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
70
|
+
created_at timestamptz not null default now(),
|
|
71
|
+
updated_at timestamptz not null default now(),
|
|
72
|
+
unique (org_id, slug)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
-- 3. roles (org-scoped, custom roles permitidos)
|
|
76
|
+
create table public.roles (
|
|
77
|
+
id uuid primary key default gen_random_uuid(),
|
|
78
|
+
org_id uuid not null references public.organizations(id) on delete cascade,
|
|
79
|
+
name text not null check (name ~ '^[a-z_]+$'),
|
|
80
|
+
description text,
|
|
81
|
+
is_built_in boolean not null default false,
|
|
82
|
+
created_at timestamptz not null default now(),
|
|
83
|
+
unique (org_id, name)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
-- 4. permissions (catálogo global)
|
|
87
|
+
create table public.permissions (
|
|
88
|
+
id uuid primary key default gen_random_uuid(),
|
|
89
|
+
action text not null check (action ~ '^[a-z_]+$'),
|
|
90
|
+
resource text not null check (resource ~ '^[a-z_]+$'),
|
|
91
|
+
description text,
|
|
92
|
+
created_at timestamptz not null default now(),
|
|
93
|
+
unique (action, resource)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
-- 5. role_permissions (M:N — roles ganham permissions)
|
|
97
|
+
create table public.role_permissions (
|
|
98
|
+
role_id uuid not null references public.roles(id) on delete cascade,
|
|
99
|
+
permission_id uuid not null references public.permissions(id) on delete restrict,
|
|
100
|
+
created_at timestamptz not null default now(),
|
|
101
|
+
primary key (role_id, permission_id)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
-- 6. organization_members (user ↔ org com role)
|
|
105
|
+
create table public.organization_members (
|
|
106
|
+
id uuid primary key default gen_random_uuid(),
|
|
107
|
+
org_id uuid not null references public.organizations(id) on delete cascade,
|
|
108
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
109
|
+
role_id uuid not null references public.roles(id) on delete restrict,
|
|
110
|
+
status text not null default 'active' check (status in ('active', 'suspended', 'left')),
|
|
111
|
+
joined_at timestamptz not null default now(),
|
|
112
|
+
unique (org_id, user_id)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
-- 7. department_members (user ↔ dept com role override opcional)
|
|
116
|
+
create table public.department_members (
|
|
117
|
+
id uuid primary key default gen_random_uuid(),
|
|
118
|
+
dept_id uuid not null references public.departments(id) on delete cascade,
|
|
119
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
120
|
+
role_id uuid references public.roles(id) on delete set null, -- NULL herda do organization_members
|
|
121
|
+
is_leader boolean not null default false,
|
|
122
|
+
joined_at timestamptz not null default now(),
|
|
123
|
+
unique (dept_id, user_id)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
-- Slug history (suporte a redirect 301 quando slug é mutado)
|
|
127
|
+
create table public.organization_slug_history (
|
|
128
|
+
id uuid primary key default gen_random_uuid(),
|
|
129
|
+
org_id uuid not null references public.organizations(id) on delete cascade,
|
|
130
|
+
old_slug text not null,
|
|
131
|
+
new_slug text not null,
|
|
132
|
+
changed_at timestamptz not null default now(),
|
|
133
|
+
unique (old_slug)
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### JWT claims minimal — Custom Access Token Hook
|
|
138
|
+
|
|
139
|
+
```sql
|
|
140
|
+
-- Hook chamado pelo Supabase Auth a cada token emit
|
|
141
|
+
-- Injeta apenas super_admin no app_metadata
|
|
142
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
143
|
+
returns jsonb
|
|
144
|
+
language plpgsql
|
|
145
|
+
security definer
|
|
146
|
+
set search_path = ''
|
|
147
|
+
as $$
|
|
148
|
+
declare
|
|
149
|
+
claims jsonb;
|
|
150
|
+
is_super boolean;
|
|
151
|
+
begin
|
|
152
|
+
-- Buscar super_admin do app_metadata atual
|
|
153
|
+
select coalesce((raw_app_meta_data->>'super_admin')::boolean, false)
|
|
154
|
+
into is_super
|
|
155
|
+
from auth.users
|
|
156
|
+
where id = (event->>'user_id')::uuid;
|
|
157
|
+
|
|
158
|
+
claims := event->'claims';
|
|
159
|
+
claims := jsonb_set(claims, '{app_metadata}', coalesce(claims->'app_metadata', '{}'::jsonb));
|
|
160
|
+
claims := jsonb_set(claims, '{app_metadata,super_admin}', to_jsonb(is_super));
|
|
161
|
+
|
|
162
|
+
event := jsonb_set(event, '{claims}', claims);
|
|
163
|
+
return event;
|
|
164
|
+
end;
|
|
165
|
+
$$;
|
|
166
|
+
|
|
167
|
+
-- Registrar como hook em supabase/config.toml:
|
|
168
|
+
-- [auth.hook.custom_access_token]
|
|
169
|
+
-- enabled = true
|
|
170
|
+
-- uri = "pg-functions://postgres/public/custom_access_token_hook"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Set super_admin via service_role apenas
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// Edge Function ou backend admin com service_role key
|
|
177
|
+
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
|
178
|
+
|
|
179
|
+
const admin = createClient(
|
|
180
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
181
|
+
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // service role — nunca expor ao client
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
// Promover usuário a super_admin
|
|
185
|
+
await admin.auth.admin.updateUserById(userId, {
|
|
186
|
+
app_metadata: { super_admin: true }
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// IMPORTANTE: usar updateUserById com app_metadata (não user_metadata)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Slug com redirect trail
|
|
193
|
+
|
|
194
|
+
```sql
|
|
195
|
+
-- Trigger que registra mudança de slug em organization_slug_history
|
|
196
|
+
create or replace function public.track_org_slug_change()
|
|
197
|
+
returns trigger
|
|
198
|
+
language plpgsql
|
|
199
|
+
security invoker
|
|
200
|
+
set search_path = ''
|
|
201
|
+
as $$
|
|
202
|
+
begin
|
|
203
|
+
if old.slug is distinct from new.slug then
|
|
204
|
+
insert into public.organization_slug_history (org_id, old_slug, new_slug)
|
|
205
|
+
values (new.id, old.slug, new.slug);
|
|
206
|
+
end if;
|
|
207
|
+
return new;
|
|
208
|
+
end;
|
|
209
|
+
$$;
|
|
210
|
+
|
|
211
|
+
create trigger track_org_slug_change_trigger
|
|
212
|
+
after update of slug on public.organizations
|
|
213
|
+
for each row execute function public.track_org_slug_change();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
App side (Next.js middleware): se `slug` em URL não existe em `organizations`, consulta `organization_slug_history.old_slug` → 301 para `/orgs/{new_slug}/`.
|
|
217
|
+
|
|
218
|
+
## Anti-patterns
|
|
219
|
+
|
|
220
|
+
### Anti-pattern 1: Lista de orgs no JWT (claims bloat + stale)
|
|
221
|
+
|
|
222
|
+
**Errado:**
|
|
223
|
+
```sql
|
|
224
|
+
-- Hook injeta lista de orgs do user no JWT
|
|
225
|
+
-- claims.app_metadata.orgs = [{id, role}, {id, role}, ...]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Por quê:**
|
|
229
|
+
- JWT tem limite ~4KB — usuário em 50 orgs estoura
|
|
230
|
+
- JWT cacheado por 1h — mudança de role demora até 1h pra propagar
|
|
231
|
+
- Cada Edge Function paga overhead de parsing de claims grandes
|
|
232
|
+
|
|
233
|
+
**Certo:** JWT só com `super_admin: bool`. Helper functions PG (`private.is_member_of`, `private.has_role`) consultam o banco — fonte de verdade sem stale.
|
|
234
|
+
|
|
235
|
+
### Anti-pattern 2: Slug mutável sem redirect trail
|
|
236
|
+
|
|
237
|
+
**Errado:**
|
|
238
|
+
```sql
|
|
239
|
+
update public.organizations set slug = 'new-name' where id = '...';
|
|
240
|
+
-- Sem entry em organization_slug_history
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Por quê:** bookmarks externos, webhooks Stripe/Slack, OAuth callbacks, Twitter cards, sitemaps — todos quebram silenciosamente. Cliente B2B descobre semanas depois quando recebe email "seu link parou de funcionar".
|
|
244
|
+
|
|
245
|
+
**Certo:** trigger `track_org_slug_change` automático + middleware redirect 301 em rotas com slug.
|
|
246
|
+
|
|
247
|
+
### Anti-pattern 3: Schema-per-tenant sem justificativa de compliance
|
|
248
|
+
|
|
249
|
+
**Errado:**
|
|
250
|
+
```sql
|
|
251
|
+
-- Pra cada nova org, criar schema próprio
|
|
252
|
+
create schema "org_acme";
|
|
253
|
+
create table org_acme.members (...);
|
|
254
|
+
-- ...repetir migration por org
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Por quê:** N migrations por nova org, provisioning lento, queries cross-tenant impossíveis sem UNION manual, monitoring complexo. Sem ganho real para SaaS comum (RLS bem feita já isola).
|
|
258
|
+
|
|
259
|
+
**Certo:** Single Schema + `org_id` + RLS. Schema-per-tenant só quando regulatório exige.
|
|
260
|
+
|
|
261
|
+
### Anti-pattern 4: `user_metadata` para super_admin
|
|
262
|
+
|
|
263
|
+
**Errado:**
|
|
264
|
+
```sql
|
|
265
|
+
-- Policy lê super_admin de user_metadata
|
|
266
|
+
using ((auth.jwt()->'user_metadata'->>'super_admin')::boolean = true)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Por quê:** `user_metadata` é editável pelo cliente via `auth.updateUser({ data: { super_admin: true } })`. Privilege escalation imediato — qualquer usuário se torna super_admin. Documentado em [Supabase Splinter 0015](https://supabase.github.io/splinter/0015_rls_references_user_metadata/).
|
|
270
|
+
|
|
271
|
+
**Certo:** `app_metadata.super_admin` (set apenas via service_role).
|
|
272
|
+
|
|
273
|
+
### Anti-pattern 5: FKs sem CASCADE/RESTRICT explícito
|
|
274
|
+
|
|
275
|
+
**Errado:**
|
|
276
|
+
```sql
|
|
277
|
+
references public.organizations(id) -- default ON DELETE NO ACTION
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Por quê:** decisão importante (deletar org com 100k rows? bloquear?) fica implícita. NO ACTION pode falhar deleção sem mensagem clara. Comportamento varia por engine.
|
|
281
|
+
|
|
282
|
+
**Certo:**
|
|
283
|
+
```sql
|
|
284
|
+
references public.organizations(id) on delete cascade -- entidade dependente
|
|
285
|
+
references auth.users(id) on delete restrict -- prevenir orfão
|
|
286
|
+
references public.roles(id) on delete set null -- preservar histórico
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Ver também
|
|
290
|
+
|
|
291
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — anti-patterns RLS herdados (`(select auth.uid())` wrapper, no `user_metadata` em authz)
|
|
292
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — padrões PG functions (security invoker, search_path = '')
|
|
293
|
+
- [supabase-postgres-style](../supabase-postgres-style/SKILL.md) — naming snake_case, lowercase reserved
|
|
294
|
+
- [multi-tenant-rls-hierarchy](../multi-tenant-rls-hierarchy/SKILL.md) — 4 helper functions PG canônicas + policies hierárquicas (Phase 108)
|
|
295
|
+
- [multi-tenant-performance-scaling](../multi-tenant-performance-scaling/SKILL.md) — Supavisor pooling + partitioning por `org_id` + MVs per-tenant (skill irmã)
|
|
296
|
+
- [rbac-permissions-matrix-supabase](../rbac-permissions-matrix-supabase/SKILL.md) — modelagem permissions action × resource × scope (Phase 108)
|
|
297
|
+
- [_shared-supabase/glossary.md](../_shared-supabase/glossary.md) — termos canônicos `app_metadata`, `service_role`
|
|
298
|
+
- [_shared-multi-tenant/glossary.md](../_shared-multi-tenant/glossary.md) — termos novos `tenant`, `org_id`, `super_admin`, `RBAC`
|
|
299
|
+
- [Supabase RLS Best Practices](https://makerkit.dev/blog/tutorials/supabase-rls-best-practices) — fonte external canônica
|
|
300
|
+
- [Custom Access Token Hook — Supabase Docs](https://supabase.com/docs/guides/auth/auth-hooks/custom-access-token-hook)
|