nexus-core-v3 3.0.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 -0
- package/README.md +134 -0
- package/agents/README.md +133 -0
- package/agents/_protocol.md +107 -0
- package/agents/analyst.md +138 -0
- package/agents/architect.md +146 -0
- package/agents/data-engineer.md +170 -0
- package/agents/dev.md +134 -0
- package/agents/devops.md +141 -0
- package/agents/nexus-master.md +147 -0
- package/agents/pm.md +133 -0
- package/agents/po.md +138 -0
- package/agents/qa.md +192 -0
- package/agents/sm.md +122 -0
- package/agents/squad-creator.md +121 -0
- package/agents/ux-design-expert.md +165 -0
- package/artifact-manifest.json +903 -0
- package/bin/nexus.mjs +37 -0
- package/checklists/README.md +49 -0
- package/checklists/architect-checklist.md +47 -0
- package/checklists/change-checklist.md +61 -0
- package/checklists/db-predeploy-checklist.md +57 -0
- package/checklists/design-quality-checklist.md +57 -0
- package/checklists/discovery-checklist.md +36 -0
- package/checklists/foundation-checklist.md +39 -0
- package/checklists/launch-checklist.md +39 -0
- package/checklists/pm-checklist.md +48 -0
- package/checklists/po-master-checklist.md +64 -0
- package/checklists/reality-check-checklist.md +49 -0
- package/checklists/story-dod-checklist.md +52 -0
- package/checklists/story-draft-checklist.md +36 -0
- package/dist/bin/dashboard.html +279 -0
- package/dist/bin/nexus.mjs +20008 -0
- package/dist/constitution.yaml +76 -0
- package/knowledge/README.md +57 -0
- package/knowledge/architecture/architectural-styles-map.md +182 -0
- package/knowledge/architecture/design-patterns-gof.md +192 -0
- package/knowledge/architecture/distributed-patterns-cheatsheet.md +201 -0
- package/knowledge/architecture/saas-subscription-blueprint.md +355 -0
- package/knowledge/architecture/system-design-tradeoffs.md +231 -0
- package/knowledge/architecture/t3-fullstack-typesafe-stack.md +273 -0
- package/knowledge/copy/landing-copy-that-converts.md +168 -0
- package/knowledge/data/postgres-indexing-and-tuning.md +263 -0
- package/knowledge/data/schema-modeling-decisions.md +273 -0
- package/knowledge/data/supabase-rls-patterns.md +316 -0
- package/knowledge/data/zero-downtime-migrations.md +308 -0
- package/knowledge/devops/cicd-pipeline-best-practices.md +318 -0
- package/knowledge/devops/production-dockerfile.md +283 -0
- package/knowledge/devops/twelve-factor-app.md +398 -0
- package/knowledge/engineering/clean-code-principles.md +429 -0
- package/knowledge/engineering/effective-code-review.md +204 -0
- package/knowledge/engineering/testing-strategy-beyond-unit.md +307 -0
- package/knowledge/governance/risk-matrix.md +56 -0
- package/knowledge/integration/mcp-server-selection-matrix.md +235 -0
- package/knowledge/marketing/copy-que-converte.md +43 -0
- package/knowledge/marketing/funil-e-jornada.md +36 -0
- package/knowledge/negocios/proposta-vencedora.md +38 -0
- package/knowledge/negocios/roi-e-unit-economics.md +46 -0
- package/knowledge/pipeline/1-descobrir.md +26 -0
- package/knowledge/pipeline/2-estrategizar.md +26 -0
- package/knowledge/pipeline/3-estruturar.md +27 -0
- package/knowledge/pipeline/4-construir.md +27 -0
- package/knowledge/pipeline/5-endurecer.md +28 -0
- package/knowledge/pipeline/6-lancar.md +27 -0
- package/knowledge/pipeline/7-operar.md +27 -0
- package/knowledge/security/lgpd-conformidade-basica.md +35 -0
- package/knowledge/security/owasp-secure-coding-gates.md +220 -0
- package/knowledge/security/owasp-top10-threat-assessment.md +287 -0
- package/knowledge/security/threat-modeling-stride.md +34 -0
- package/knowledge/web-craft/a11y-audit-checklist.md +251 -0
- package/knowledge/web-craft/accessible-component-patterns.md +383 -0
- package/knowledge/web-craft/anti-ai-look.md +114 -0
- package/knowledge/web-craft/design-system-from-code.md +195 -0
- package/knowledge/web-craft/intrinsic-css-layout.md +420 -0
- package/knowledge/web-craft/style-cloning.md +185 -0
- package/knowledge/web-craft/visual-polish-review.md +183 -0
- package/package.json +55 -0
- package/runbooks/campanha-de-conteudo.md +36 -0
- package/runbooks/feature-em-projeto-existente.md +37 -0
- package/runbooks/mvp-startup.md +38 -0
- package/runbooks/resposta-a-incidente.md +37 -0
- package/squads/exemplo-conteudo/agents/editor-chefe.md +48 -0
- package/squads/exemplo-conteudo/agents/pesquisador.md +44 -0
- package/squads/exemplo-conteudo/agents/redator.md +45 -0
- package/squads/exemplo-conteudo/knowledge/estilo-editorial.md +21 -0
- package/squads/exemplo-conteudo/squad.yaml +19 -0
- package/squads/exemplo-conteudo/tasks/pesquisar-fontes.md +26 -0
- package/squads/exemplo-conteudo/tasks/planejar-pauta.md +27 -0
- package/squads/exemplo-conteudo/tasks/redigir-artigo.md +26 -0
- package/squads/exemplo-conteudo/tasks/revisar-artigo.md +27 -0
- package/squads/marketing/agents/analista.md +56 -0
- package/squads/marketing/agents/chefe-marketing.md +65 -0
- package/squads/marketing/agents/conteudo.md +55 -0
- package/squads/marketing/agents/copy.md +55 -0
- package/squads/marketing/agents/growth.md +56 -0
- package/squads/marketing/agents/social.md +55 -0
- package/squads/marketing/squad.yaml +17 -0
- package/squads/marketing/tasks/aprovar-campanha.md +43 -0
- package/squads/negocios/agents/chefe-negocios.md +65 -0
- package/squads/negocios/agents/financas-roi.md +55 -0
- package/squads/negocios/agents/suporte.md +55 -0
- package/squads/negocios/agents/vendas-proposta.md +56 -0
- package/squads/negocios/squad.yaml +17 -0
- package/squads/negocios/tasks/aprovar-proposta.md +40 -0
- package/squads/security/agents/appsec-reviewer.md +59 -0
- package/squads/security/agents/chefe-seguranca.md +65 -0
- package/squads/security/agents/compliance-auditor.md +60 -0
- package/squads/security/agents/threat-modeler.md +60 -0
- package/squads/security/squad.yaml +20 -0
- package/squads/security/tasks/aprovar-gate-seguranca.md +42 -0
- package/squads/security/tasks/emitir-parecer-conformidade.md +42 -0
- package/tasks/README.md +72 -0
- package/tasks/accessibility-wcag-checklist.md +69 -0
- package/tasks/advanced-elicitation.md +42 -0
- package/tasks/analyze-performance.md +54 -0
- package/tasks/analyze-project-structure.md +59 -0
- package/tasks/apply-qa-fixes.md +57 -0
- package/tasks/architect-analyze-impact.md +62 -0
- package/tasks/archive-squad.md +52 -0
- package/tasks/audit-codebase.md +53 -0
- package/tasks/build-component.md +61 -0
- package/tasks/calculate-roi.md +63 -0
- package/tasks/ci-cd-configuration.md +51 -0
- package/tasks/collect-visual-evidence.md +62 -0
- package/tasks/compose-molecule.md +57 -0
- package/tasks/consolidate-patterns.md +54 -0
- package/tasks/create-brownfield-prd.md +54 -0
- package/tasks/create-competitor-analysis.md +42 -0
- package/tasks/create-deep-research-prompt.md +62 -0
- package/tasks/create-doc.md +62 -0
- package/tasks/create-epic.md +49 -0
- package/tasks/create-front-end-spec.md +56 -0
- package/tasks/create-migration-plan.md +57 -0
- package/tasks/create-next-story.md +66 -0
- package/tasks/create-prd.md +53 -0
- package/tasks/create-project-brief.md +47 -0
- package/tasks/create-rls-policies.md +59 -0
- package/tasks/create-schema.md +57 -0
- package/tasks/create-service.md +55 -0
- package/tasks/create-squad.md +100 -0
- package/tasks/create-suite.md +62 -0
- package/tasks/db-apply-migration.md +56 -0
- package/tasks/db-domain-modeling.md +57 -0
- package/tasks/db-dry-run.md +50 -0
- package/tasks/db-env-check.md +57 -0
- package/tasks/db-load-csv.md +54 -0
- package/tasks/db-policy-apply.md +58 -0
- package/tasks/db-rollback.md +51 -0
- package/tasks/db-run-sql.md +61 -0
- package/tasks/db-seed.md +52 -0
- package/tasks/db-smoke-test.md +51 -0
- package/tasks/db-snapshot.md +48 -0
- package/tasks/db-verify-order.md +49 -0
- package/tasks/deliberate.md +46 -0
- package/tasks/design-indexes.md +59 -0
- package/tasks/dev-develop-story.md +61 -0
- package/tasks/document-project.md +59 -0
- package/tasks/execute-checklist.md +57 -0
- package/tasks/execute-epic-plan.md +52 -0
- package/tasks/execute-subtask.md +51 -0
- package/tasks/extend-pattern.md +63 -0
- package/tasks/extend-squad.md +60 -0
- package/tasks/extract-patterns.md +64 -0
- package/tasks/extract-tokens.md +59 -0
- package/tasks/facilitate-brainstorming-session.md +42 -0
- package/tasks/generate-ai-frontend-prompt.md +57 -0
- package/tasks/generate-documentation.md +60 -0
- package/tasks/generate-migration-strategy.md +57 -0
- package/tasks/generate-shock-report.md +56 -0
- package/tasks/mcp-management.md +66 -0
- package/tasks/orchestrate.md +50 -0
- package/tasks/perform-market-research.md +42 -0
- package/tasks/plan-create-context.md +57 -0
- package/tasks/plan-create-implementation.md +58 -0
- package/tasks/po-close-story.md +60 -0
- package/tasks/po-manage-story-backlog.md +59 -0
- package/tasks/po-pull-story.md +60 -0
- package/tasks/po-sync-story.md +59 -0
- package/tasks/pr-automation.md +50 -0
- package/tasks/pre-push-quality-gate.md +54 -0
- package/tasks/push.md +53 -0
- package/tasks/qa-browser-console-check.md +52 -0
- package/tasks/qa-create-fix-request.md +58 -0
- package/tasks/qa-evidence-requirements.md +55 -0
- package/tasks/qa-false-positive-detection.md +55 -0
- package/tasks/qa-fix-issues.md +55 -0
- package/tasks/qa-gate.md +53 -0
- package/tasks/qa-migration-validation.md +58 -0
- package/tasks/qa-nfr-assess.md +45 -0
- package/tasks/qa-review-story.md +56 -0
- package/tasks/qa-risk-profile.md +45 -0
- package/tasks/qa-security-checklist.md +64 -0
- package/tasks/qa-test-design.md +47 -0
- package/tasks/qa-trace-requirements.md +48 -0
- package/tasks/release-management.md +53 -0
- package/tasks/repository-cleanup.md +61 -0
- package/tasks/route.md +44 -0
- package/tasks/run-tests.md +50 -0
- package/tasks/security-audit.md +54 -0
- package/tasks/setup-database.md +60 -0
- package/tasks/setup-design-system.md +60 -0
- package/tasks/shard-doc.md +60 -0
- package/tasks/spec-assess-complexity.md +55 -0
- package/tasks/spec-critique.md +64 -0
- package/tasks/spec-gather-requirements.md +48 -0
- package/tasks/spec-research-dependencies.md +42 -0
- package/tasks/spec-write-spec.md +50 -0
- package/tasks/test-as-user.md +52 -0
- package/tasks/ux-create-wireframe.md +54 -0
- package/tasks/ux-user-research.md +55 -0
- package/tasks/validate-next-story.md +61 -0
- package/tasks/validate-squad.md +55 -0
- package/tasks/verify-subtask.md +52 -0
- package/tasks/version-management.md +45 -0
- package/templates/README.md +47 -0
- package/templates/architecture-tmpl.md +115 -0
- package/templates/competitor-analysis-tmpl.md +87 -0
- package/templates/epic-tmpl.md +83 -0
- package/templates/front-end-spec-tmpl.md +110 -0
- package/templates/market-research-tmpl.md +98 -0
- package/templates/migration-plan-tmpl.md +92 -0
- package/templates/prd-tmpl.md +95 -0
- package/templates/project-brief-tmpl.md +100 -0
- package/templates/qa-verdict-tmpl.md +73 -0
- package/templates/rls-policies-tmpl.md +93 -0
- package/templates/schema-design-tmpl.md +107 -0
- package/templates/spec-tmpl.md +88 -0
- package/templates/squad/agent-dna-tmpl.md +72 -0
- package/templates/squad/chief-dna-tmpl.md +98 -0
- package/templates/squad/squad-task-tmpl.md +50 -0
- package/templates/squad/squad-yaml-tmpl.md +47 -0
- package/templates/story-tmpl.md +63 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-modeling-decisions
|
|
3
|
+
domain: data
|
|
4
|
+
agents: [data-engineer]
|
|
5
|
+
when: "ao modelar o schema de uma aplicação"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Decisões de modelagem de schema — normalizar, chaves, relacionamentos e árvores
|
|
9
|
+
|
|
10
|
+
A maioria dos schemas ruins não nasce de ignorância: nasce de defaults aplicados sem critério.
|
|
11
|
+
Coluna `id` serial em toda tabela porque o ORM gera; lista separada por vírgula porque "é só um
|
|
12
|
+
campo"; `parent_id` numa árvore porque era o primeiro padrão que apareceu. Bill Karwin catalogou
|
|
13
|
+
esses defaults como **antipatterns** — cada um tem uma cara reconhecível e uma correção concreta.
|
|
14
|
+
Este pack é a árvore de decisão de modelagem: quando normalizar, qual chave usar, como traduzir
|
|
15
|
+
cardinalidade em FK, e como modelar hierarquia sem cair no antipattern. A régua-mãe (Karwin):
|
|
16
|
+
**"A Primary Key is a constraint, not a data type"** — e o mesmo vale para todo o schema. Você
|
|
17
|
+
escolhe a estrutura pela restrição que ela garante, não pelo que o framework gera por padrão.
|
|
18
|
+
|
|
19
|
+
## O problema (os tells de um schema gerado, não desenhado)
|
|
20
|
+
|
|
21
|
+
Reconheça para evitar. Se o schema tem 3+ destes, ele foi gerado, não modelado:
|
|
22
|
+
|
|
23
|
+
1. **Jaywalking** — múltiplos valores numa coluna separados por vírgula (`tags = "sql,db,perf"`,
|
|
24
|
+
`account_ids = "12,34,56"`). Viola a 1NF. Karwin chama de *jaywalking* porque é "atravessar fora
|
|
25
|
+
da faixa" para evitar criar a tabela de interseção (*intersection*).
|
|
26
|
+
2. **Multicolumn Attributes** — `tag1, tag2, tag3` em colunas separadas. Mesmo problema da
|
|
27
|
+
Jaywalking, só que na horizontal: como você consulta "todas as linhas com a tag X" sem repetir
|
|
28
|
+
`WHERE tag1=X OR tag2=X OR tag3=X`?
|
|
29
|
+
3. **ID Required** — coluna `id` serial em *toda* tabela por reflexo, inclusive onde uma chave
|
|
30
|
+
natural ou composta seria melhor (sobretudo em tabelas de interseção).
|
|
31
|
+
4. **Keyless Entry** — relacionamentos sem `FOREIGN KEY`. A integridade vira responsabilidade da
|
|
32
|
+
aplicação, e qualquer script externo (migração, import, job) gera linhas órfãs.
|
|
33
|
+
5. **Naive Trees** — hierarquia modelada só com `parent_id` (adjacency list) sem pensar no padrão
|
|
34
|
+
de consulta. "Pegar todos os descendentes" vira N joins ou recursão na aplicação.
|
|
35
|
+
6. **EAV (Entity-Attribute-Value)** — tabela genérica `entity_id | attribute_name | value` para
|
|
36
|
+
"schema flexível". Você perde tipo, `NOT NULL`, FK e qualquer consulta sã.
|
|
37
|
+
7. **Polymorphic Associations** — uma FK que aponta para "várias tabelas" via coluna
|
|
38
|
+
discriminadora (`commentable_id` + `commentable_type`). SQL não suporta FK assim → sem
|
|
39
|
+
integridade referencial.
|
|
40
|
+
|
|
41
|
+
## O conhecimento / os princípios
|
|
42
|
+
|
|
43
|
+
### 1. Normalizar é o default. Desnormalizar é uma exceção justificada.
|
|
44
|
+
|
|
45
|
+
Normalize até a 3NF como ponto de partida. As três primeiras formas normais resolvem os
|
|
46
|
+
antipatterns mais comuns:
|
|
47
|
+
|
|
48
|
+
| Forma | Regra (Karwin) | Antipattern que elimina |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| **1NF** | Sem colunas repetidas nem valores separados por vírgula numa célula. Cada célula = um valor atômico. | Jaywalking, Multicolumn Attributes |
|
|
51
|
+
| **2NF** | Com chave composta, nenhuma coluna pode depender de só *parte* da chave. | Redundância em tabelas de junção |
|
|
52
|
+
| **3NF** | Proíbe armazenar dado não relacionado à PK e duplicado em outro lugar. | Anomalias de update |
|
|
53
|
+
|
|
54
|
+
> Karwin: ir além de 3NF/BCNF para 4NF+ é raramente necessário e custa muitos joins. 3NF é o alvo
|
|
55
|
+
> prático para a maioria das aplicações.
|
|
56
|
+
|
|
57
|
+
**Desnormalize só com critério explícito** (não "por performance" no chute). Critérios válidos:
|
|
58
|
+
|
|
59
|
+
- **Read-heavy comprovado** — a tabela é lida ordens de magnitude mais do que escrita, e o join
|
|
60
|
+
está medido como gargalo (não suposto).
|
|
61
|
+
- **Dado histórico/snapshot** — você *quer* congelar o valor no momento (preço da `order_item` na
|
|
62
|
+
hora da compra, não o preço atual do produto). Aqui "duplicar" o preço não é desnormalização — é
|
|
63
|
+
o modelo correto, porque a semântica é "valor naquele instante".
|
|
64
|
+
- **Agregado materializado** — `COUNT`/`SUM` pré-calculado (ex.: `post.comments_count`) com
|
|
65
|
+
trigger ou job mantendo a consistência.
|
|
66
|
+
|
|
67
|
+
Regra: se você não consegue nomear **qual escrita vai manter o dado redundante consistente**, não
|
|
68
|
+
desnormalize. A redundância sem dono é bug esperando acontecer.
|
|
69
|
+
|
|
70
|
+
### 2. Jaywalking / Multicolumn → tabela de interseção (1NF)
|
|
71
|
+
|
|
72
|
+
O tell:
|
|
73
|
+
|
|
74
|
+
```sql
|
|
75
|
+
-- ANTIPATTERN (Jaywalking): N:N escondido numa string
|
|
76
|
+
CREATE TABLE products (
|
|
77
|
+
product_id SERIAL PRIMARY KEY,
|
|
78
|
+
account_ids VARCHAR(100) -- "12,34,56"
|
|
79
|
+
);
|
|
80
|
+
-- "Quem é dono do produto 5?" → WHERE account_ids LIKE '%5%' (pega 15, 25, 51...)
|
|
81
|
+
-- COUNT/SUM/JOIN/FK: impossíveis. Limite de tamanho da coluna: bomba-relógio.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A correção é sempre a **tabela de interseção**, com a chave composta dos dois lados como PK
|
|
85
|
+
(que também elimina duplicatas de graça):
|
|
86
|
+
|
|
87
|
+
```sql
|
|
88
|
+
CREATE TABLE contacts (
|
|
89
|
+
product_id INT NOT NULL REFERENCES products(product_id),
|
|
90
|
+
account_id INT NOT NULL REFERENCES accounts(account_id),
|
|
91
|
+
PRIMARY KEY (product_id, account_id) -- chave composta natural, sem 'id' serial
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
O mesmo vale para `tag1, tag2, tag3` (Multicolumn): vira `post_tags(post_id, tag_id)`.
|
|
96
|
+
|
|
97
|
+
### 3. PK: surrogate (pseudokey) vs. natural vs. composta
|
|
98
|
+
|
|
99
|
+
Karwin: o antipattern **ID Required** não é "usar `id` serial" — é usar `id` serial *sem pensar*,
|
|
100
|
+
em toda tabela. A decisão real:
|
|
101
|
+
|
|
102
|
+
```sql
|
|
103
|
+
-- Tabela de interseção: chave COMPOSTA NATURAL é melhor que 'id' serial.
|
|
104
|
+
-- Garante unicidade do par (impede duplicata) sem índice extra.
|
|
105
|
+
PRIMARY KEY (product_id, account_id)
|
|
106
|
+
|
|
107
|
+
-- Entidade de domínio com identificador natural confiável e estável:
|
|
108
|
+
CREATE TABLE countries (
|
|
109
|
+
iso_code CHAR(2) PRIMARY KEY, -- 'BR', 'US' — único, estável, significativo
|
|
110
|
+
name VARCHAR(80) NOT NULL
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
-- Entidade sem chave natural estável (usuário, pedido): surrogate é legítimo.
|
|
114
|
+
CREATE TABLE orders (
|
|
115
|
+
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Critérios de Karwin para **natural/composta**:
|
|
120
|
+
- O atributo é **genuinamente único** (verifique nos dados reais, não na esperança).
|
|
121
|
+
- É **estável** (não muda — CPF muda? email muda? então não é boa PK).
|
|
122
|
+
- Carrega **significado de domínio** que simplifica queries.
|
|
123
|
+
|
|
124
|
+
Critérios para **surrogate**:
|
|
125
|
+
- Não existe chave natural estável, OU a natural é larga/composta e referenciada por muitas FKs.
|
|
126
|
+
- A imutabilidade da PK importa (surrogate nunca muda, mesmo que o "negócio" mude).
|
|
127
|
+
|
|
128
|
+
> Nota de naming: Karwin recomenda nome descritivo (`comment_id`) para permitir `JOIN ... USING
|
|
129
|
+
> (comment_id)`. Equipes que abstraem o front com convenção `id` constante discordam — escolha uma
|
|
130
|
+
> convenção e mantenha. O antipattern é não decidir.
|
|
131
|
+
|
|
132
|
+
### 4. Cardinalidade → FK (e sempre declare a FK)
|
|
133
|
+
|
|
134
|
+
Traduza a cardinalidade do domínio em estrutura. E declare `FOREIGN KEY` — **Keyless Entry** é
|
|
135
|
+
antipattern: sem a constraint, a integridade vira "torcer para a aplicação acertar", e qualquer
|
|
136
|
+
import/script externo gera órfãos.
|
|
137
|
+
|
|
138
|
+
| Cardinalidade | Onde mora a FK | Forma |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| **1:N** (um cliente, N pedidos) | Na tabela do lado "N" | `orders.customer_id REFERENCES customers` |
|
|
141
|
+
| **N:N** (produtos ↔ contas) | Tabela de interseção | `contacts(product_id, account_id)` com PK composta |
|
|
142
|
+
| **1:1** (user ↔ profile) | FK + `UNIQUE` (ou PK compartilhada) | `profiles.user_id UNIQUE REFERENCES users` |
|
|
143
|
+
| **0..1** (opcional) | FK `NULL`-able | `employees.manager_id NULL REFERENCES employees` |
|
|
144
|
+
|
|
145
|
+
Escolha a ação referencial de propósito (Karwin):
|
|
146
|
+
|
|
147
|
+
```sql
|
|
148
|
+
order_id BIGINT REFERENCES orders(order_id)
|
|
149
|
+
ON DELETE CASCADE -- itens somem com o pedido (parte fraca da composição)
|
|
150
|
+
-- ON DELETE RESTRICT -- impede apagar o pai se há filhos (default seguro)
|
|
151
|
+
-- ON DELETE SET NULL -- desvincula sem apagar (ex.: manager saiu, subordinado fica)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 5. Hierarquias → escolha o modelo pelo padrão de leitura/escrita (não use só `parent_id`)
|
|
155
|
+
|
|
156
|
+
**Naive Trees** = usar só adjacency list sem perguntar como a árvore é consultada. Os quatro
|
|
157
|
+
modelos de Karwin, com critério de escolha:
|
|
158
|
+
|
|
159
|
+
**a) Adjacency List** (`parent_id`) — o default honesto.
|
|
160
|
+
|
|
161
|
+
```sql
|
|
162
|
+
CREATE TABLE comments (
|
|
163
|
+
comment_id SERIAL PRIMARY KEY,
|
|
164
|
+
parent_id INT REFERENCES comments(comment_id),
|
|
165
|
+
body TEXT
|
|
166
|
+
);
|
|
167
|
+
-- Mover subárvore = 1 UPDATE no parent_id. Inserir = trivial.
|
|
168
|
+
-- "Todos os descendentes" precisa de recursão (PostgreSQL 8.2+):
|
|
169
|
+
WITH RECURSIVE tree AS (
|
|
170
|
+
SELECT comment_id, parent_id FROM comments WHERE comment_id = 4
|
|
171
|
+
UNION ALL
|
|
172
|
+
SELECT c.comment_id, c.parent_id
|
|
173
|
+
FROM comments c JOIN tree t ON c.parent_id = t.comment_id
|
|
174
|
+
)
|
|
175
|
+
SELECT * FROM tree;
|
|
176
|
+
```
|
|
177
|
+
Use quando: o banco suporta `WITH RECURSIVE`, inserts/moves são frequentes, e a profundidade das
|
|
178
|
+
leituras é moderada. Em Postgres moderno, é a escolha default razoável.
|
|
179
|
+
|
|
180
|
+
**b) Path Enumeration** (caminho materializado) — leitura por prefixo.
|
|
181
|
+
|
|
182
|
+
```sql
|
|
183
|
+
ALTER TABLE comments ADD COLUMN path VARCHAR(255); -- '4/5/8/'
|
|
184
|
+
-- Todos os descendentes de 4 (sem recursão):
|
|
185
|
+
SELECT * FROM comments WHERE path LIKE '4/%';
|
|
186
|
+
```
|
|
187
|
+
Use quando: leitura é dominante e por subárvore. Cuidado: mover uma subárvore exige reescrever o
|
|
188
|
+
`path` de todos os descendentes, e não há FK garantindo que o caminho aponta para nós reais.
|
|
189
|
+
|
|
190
|
+
**c) Nested Sets** (`left`/`right`) — leitura de subárvore ultrarrápida, escrita cara.
|
|
191
|
+
|
|
192
|
+
Cada nó guarda dois números (left, right); descendentes têm `left/right` *entre* os do ancestral.
|
|
193
|
+
Karwin: rápido para consultar subárvores, mas **inserir/mover é complexo** — quase todos os nós
|
|
194
|
+
precisam ter left/right renumerados. Use só quando a árvore é praticamente read-only (catálogo
|
|
195
|
+
estável) e você lê subárvores inteiras o tempo todo.
|
|
196
|
+
|
|
197
|
+
**d) Closure Table** — tabela separada com *uma linha por par ancestral→descendente*. O equilíbrio
|
|
198
|
+
de Karwin: rápido em todas as operações, ao custo de mais armazenamento.
|
|
199
|
+
|
|
200
|
+
```sql
|
|
201
|
+
CREATE TABLE comment_tree (
|
|
202
|
+
ancestor INT NOT NULL REFERENCES comments(comment_id),
|
|
203
|
+
descendant INT NOT NULL REFERENCES comments(comment_id),
|
|
204
|
+
depth INT NOT NULL,
|
|
205
|
+
PRIMARY KEY (ancestor, descendant)
|
|
206
|
+
);
|
|
207
|
+
-- Inserir nó: linha self (depth 0) + uma linha para cada ancestral do pai.
|
|
208
|
+
INSERT INTO comment_tree (ancestor, descendant, depth)
|
|
209
|
+
SELECT ancestor, :new_id, depth + 1
|
|
210
|
+
FROM comment_tree WHERE descendant = :parent_id
|
|
211
|
+
UNION ALL SELECT :new_id, :new_id, 0;
|
|
212
|
+
-- Todos os descendentes: SELECT descendant FROM comment_tree WHERE ancestor = :id;
|
|
213
|
+
-- Linha de comando ascendente: SELECT ancestor FROM comment_tree WHERE descendant = :id ORDER BY depth DESC;
|
|
214
|
+
```
|
|
215
|
+
Use quando: você precisa de consultas rápidas de ancestral *e* descendente, suporta atributos na
|
|
216
|
+
aresta (ex.: data de vigência), e as escritas são moderadas. É a escolha mais versátil do livro.
|
|
217
|
+
|
|
218
|
+
**e) `ltree` (PostgreSQL)** — path enumeration nativo, com operadores de árvore.
|
|
219
|
+
|
|
220
|
+
```sql
|
|
221
|
+
CREATE EXTENSION ltree;
|
|
222
|
+
ALTER TABLE comments ADD COLUMN path ltree; -- 'root.4.5.8'
|
|
223
|
+
CREATE INDEX ON comments USING GIST (path);
|
|
224
|
+
SELECT * FROM comments WHERE path <@ '4'; -- todos os descendentes de 4
|
|
225
|
+
```
|
|
226
|
+
Use quando: está em Postgres e quer materialized path "de fábrica". Limitações: o índice GIST tem
|
|
227
|
+
limite de tamanho de label (árvores muito profundas sofrem), e mover subárvore exige reescrever o
|
|
228
|
+
`path` de todos os descendentes.
|
|
229
|
+
|
|
230
|
+
## Checklist (passe antes de aprovar o schema)
|
|
231
|
+
|
|
232
|
+
- [ ] Alguma coluna guarda lista separada por vírgula ou `campo1/campo2/campo3`? (Jaywalking /
|
|
233
|
+
Multicolumn → tabela de interseção)
|
|
234
|
+
- [ ] Toda relação tem `FOREIGN KEY` declarada com ação `ON DELETE`/`ON UPDATE` escolhida de
|
|
235
|
+
propósito? (Keyless Entry)
|
|
236
|
+
- [ ] Cada tabela de interseção usa **chave composta** dos dois lados como PK, não um `id` serial
|
|
237
|
+
desnecessário? (ID Required)
|
|
238
|
+
- [ ] Onde existe chave natural estável e única, ela foi considerada como PK em vez de surrogate
|
|
239
|
+
automático?
|
|
240
|
+
- [ ] Existe alguma FK "polimórfica" (`*_id` + `*_type`)? Se sim, refatorada para arco exclusivo ou
|
|
241
|
+
interseções por tipo?
|
|
242
|
+
- [ ] Existe tabela genérica `entity/attribute/value`? Se sim, substituída por colunas tipadas,
|
|
243
|
+
tabela por subtipo, ou JSON tipado?
|
|
244
|
+
- [ ] A hierarquia foi modelada pelo padrão de leitura/escrita real, não só `parent_id` por reflexo?
|
|
245
|
+
- [ ] Toda redundância (dado duplicado) tem um dono explícito — a escrita que a mantém consistente?
|
|
246
|
+
|
|
247
|
+
## Tabela de decisão "use X quando Y"
|
|
248
|
+
|
|
249
|
+
| Use X | Quando Y |
|
|
250
|
+
|---|---|
|
|
251
|
+
| **Normalizar até 3NF** | Default. Sempre comece aqui. |
|
|
252
|
+
| **Desnormalizar** | Read-heavy *medido*, snapshot histórico, ou agregado materializado **com dono de consistência** |
|
|
253
|
+
| **Tabela de interseção** (PK composta) | Cardinalidade N:N — sempre, nunca lista CSV nem `tag1/tag2/tag3` |
|
|
254
|
+
| **PK surrogate** (`IDENTITY`/serial) | Sem chave natural estável; natural muito larga; imutabilidade da PK importa |
|
|
255
|
+
| **PK natural** | Atributo único, estável e significativo (`iso_code`, código de referência) |
|
|
256
|
+
| **PK composta** | Tabela de interseção (os dois FKs juntos) |
|
|
257
|
+
| **FK `ON DELETE CASCADE`** | Filho é parte fraca da composição (item do pedido some com o pedido) |
|
|
258
|
+
| **FK `ON DELETE RESTRICT`** | Default seguro: impede apagar pai com filhos |
|
|
259
|
+
| **FK `ON DELETE SET NULL`** | Vínculo opcional que pode existir sozinho (subordinado sem gestor) |
|
|
260
|
+
| **Adjacency List** (`parent_id`) | Postgres com `WITH RECURSIVE`; inserts/moves frequentes; profundidade moderada |
|
|
261
|
+
| **Path Enumeration / `ltree`** | Leitura por subárvore dominante; árvore não muito profunda; moves raros |
|
|
262
|
+
| **Nested Sets** | Árvore quase read-only; leitura de subárvore inteira é o caso crítico |
|
|
263
|
+
| **Closure Table** | Precisa de queries rápidas de ancestral E descendente; atributos na aresta; escrita moderada |
|
|
264
|
+
| **Colunas tipadas / tabela por subtipo** | Em vez de EAV — quando os atributos são conhecidos |
|
|
265
|
+
| **JSON/JSONB tipado** | Schema genuinamente dinâmico — em vez de EAV, mantendo o resto relacional |
|
|
266
|
+
| **Arco exclusivo** (parent table compartilhada) | Em vez de FK polimórfica — quando precisa de FK real |
|
|
267
|
+
| **Interseções por tipo** | Em vez de FK polimórfica — quando a 1:1 não precisa ser forçada |
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
Fonte: Bill Karwin, *SQL Antipatterns: Avoiding the Pitfalls of Database Programming* (Pragmatic
|
|
272
|
+
Bookshelf) — capítulos Jaywalking, Multicolumn Attributes, Naive Trees, ID Required, Keyless Entry,
|
|
273
|
+
Entity-Attribute-Value, Polymorphic Associations; e a coletânea *awesome-database-design*.
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: supabase-rls-patterns
|
|
3
|
+
domain: data
|
|
4
|
+
agents: [data-engineer]
|
|
5
|
+
when: "ao implementar segurança de dados com Row Level Security no Supabase/Postgres"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Supabase RLS — segurança no banco, não na aplicação
|
|
9
|
+
|
|
10
|
+
## O problema
|
|
11
|
+
|
|
12
|
+
Com Supabase, qualquer cliente com a `anon` key fala direto com o Postgres via PostgREST. Não existe
|
|
13
|
+
"backend que filtra antes". Se você confia no `.eq('user_id', userId)` do cliente para isolar dados, a
|
|
14
|
+
segurança é uma sugestão: qualquer um troca o `userId` no DevTools e lê a tabela inteira. **A fronteira
|
|
15
|
+
de autorização é o banco.** RLS é o mecanismo que faz o próprio Postgres decidir, linha a linha, o que
|
|
16
|
+
cada `role`/usuário pode `select`/`insert`/`update`/`delete`.
|
|
17
|
+
|
|
18
|
+
Os tells de RLS feito por quem não conhece:
|
|
19
|
+
|
|
20
|
+
1. **Tabela com RLS desabilitado** exposta no schema `public` — vazamento total via API.
|
|
21
|
+
2. **`enable row level security` sem nenhuma policy** — ninguém lê nada (RLS nega por padrão), e o dev
|
|
22
|
+
"conserta" desabilitando RLS em vez de escrever a policy.
|
|
23
|
+
3. **`auth.uid()` chamado direto na policy** (sem `(select …)`) — reavaliado **por linha**, derruba a
|
|
24
|
+
query de <1ms para segundos numa tabela de 100k linhas.
|
|
25
|
+
4. **Policy sem `to authenticated`** — roda o predicado caro até para `anon`, que nunca deveria passar.
|
|
26
|
+
5. **Policy baseada em `user_metadata`** — claim que o **próprio usuário edita**; vira escalonamento de
|
|
27
|
+
privilégio (o atacante se promove a admin).
|
|
28
|
+
6. **`update` sem `select` correspondente** — o update "não funciona" e ninguém entende por quê.
|
|
29
|
+
7. **Multi-tenant filtrando `tenant_id` só na app**, sem coluna `tenant_id` na policy nem índice — um
|
|
30
|
+
tenant lê dados do outro, e quando você adiciona a policy, a query fica lenta sem índice.
|
|
31
|
+
|
|
32
|
+
## O conhecimento / os princípios
|
|
33
|
+
|
|
34
|
+
### 1. Ligue RLS e conceda os grants — RLS nega por padrão
|
|
35
|
+
|
|
36
|
+
RLS só protege se estiver habilitado, e habilitar sem policy bloqueia tudo. Toda tabela em `public`
|
|
37
|
+
deve ter RLS ligado:
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
alter table public.todos enable row level security;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Os `grant` definem *quais operações o role pode tentar*; a policy define *quais linhas*. Os dois são
|
|
44
|
+
necessários:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
grant select on public.todos to anon;
|
|
48
|
+
grant select, insert, update, delete on public.todos to authenticated;
|
|
49
|
+
grant select, insert, update, delete on public.todos to service_role;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> Sem policy, ninguém (exceto `service_role`/`bypassrls`) lê nada. A resposta certa para "ninguém lê" é
|
|
53
|
+
> **escrever a policy**, nunca desabilitar RLS.
|
|
54
|
+
|
|
55
|
+
### 2. USING vs WITH CHECK — eles não são intercambiáveis
|
|
56
|
+
|
|
57
|
+
- **`using`** filtra as linhas **existentes** que a query pode ver/afetar (read-side).
|
|
58
|
+
- **`with check`** valida as linhas **novas/modificadas** que estão sendo gravadas (write-side).
|
|
59
|
+
|
|
60
|
+
Se você só define `using` num insert/update, não há validação do que entra. Regra por operação (direto
|
|
61
|
+
da doc do Supabase):
|
|
62
|
+
|
|
63
|
+
| Operação | `using` | `with check` |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `select` | sempre | nunca |
|
|
66
|
+
| `insert` | nunca | sempre |
|
|
67
|
+
| `update` | quase sempre | sempre |
|
|
68
|
+
| `delete` | sempre | nunca |
|
|
69
|
+
|
|
70
|
+
Postgres **não aceita múltiplas operações num único `for`** — escreva **uma policy por operação**:
|
|
71
|
+
|
|
72
|
+
```sql
|
|
73
|
+
-- SELECT: usa USING
|
|
74
|
+
create policy "Individuals can view their own todos."
|
|
75
|
+
on public.todos for select
|
|
76
|
+
to authenticated
|
|
77
|
+
using ( (select auth.uid()) = user_id );
|
|
78
|
+
|
|
79
|
+
-- INSERT: usa WITH CHECK (impede gravar linha de outro usuário)
|
|
80
|
+
create policy "Users can create their own todos."
|
|
81
|
+
on public.todos for insert
|
|
82
|
+
to authenticated
|
|
83
|
+
with check ( (select auth.uid()) = user_id );
|
|
84
|
+
|
|
85
|
+
-- UPDATE: USING (quais linhas pode tocar) + WITH CHECK (no que pode virar)
|
|
86
|
+
create policy "Users can update their own todos."
|
|
87
|
+
on public.todos for update
|
|
88
|
+
to authenticated
|
|
89
|
+
using ( (select auth.uid()) = user_id )
|
|
90
|
+
with check ( (select auth.uid()) = user_id );
|
|
91
|
+
|
|
92
|
+
-- DELETE: usa USING
|
|
93
|
+
create policy "Users can delete their own todos."
|
|
94
|
+
on public.todos for delete
|
|
95
|
+
to authenticated
|
|
96
|
+
using ( (select auth.uid()) = user_id );
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> **Armadilha:** `update` precisa de uma policy de `select` correspondente, senão o update "não
|
|
100
|
+
> funciona". O Postgres precisa enxergar a linha (`select`) para então atualizá-la.
|
|
101
|
+
|
|
102
|
+
### 3. Sempre especifique o role com `to`
|
|
103
|
+
|
|
104
|
+
Sem `to`, a policy é avaliada para **todos os roles**, inclusive `anon` — que paga o custo de um
|
|
105
|
+
predicado que nunca vai passar. Com `to authenticated`, a avaliação para cedo para visitantes.
|
|
106
|
+
|
|
107
|
+
Ordem das cláusulas: `on <tabela>` → `for <operação>` → `to <roles>` → `using/with check`.
|
|
108
|
+
|
|
109
|
+
```sql
|
|
110
|
+
-- leitura pública explícita
|
|
111
|
+
create policy "Public profiles are visible to everyone."
|
|
112
|
+
on public.profiles for select
|
|
113
|
+
to anon, authenticated
|
|
114
|
+
using ( true );
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Benchmark da doc (tabela 100k linhas): adicionar `to authenticated` numa query feita por `anon` cai de
|
|
118
|
+
**170ms para <0,1ms** — porque a policy nem roda.
|
|
119
|
+
|
|
120
|
+
### 4. auth.uid() e auth.jwt() — e o claim que você NÃO pode confiar
|
|
121
|
+
|
|
122
|
+
- `auth.uid()` → `uuid` do usuário autenticado (vem do `sub` do JWT).
|
|
123
|
+
- `auth.jwt()` → claims completos do token.
|
|
124
|
+
|
|
125
|
+
Dentro do JWT existem dois objetos de metadata, e a diferença é de segurança, não de estilo:
|
|
126
|
+
|
|
127
|
+
| Claim | Origem | Confiável em RLS? |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `app_metadata` (`raw_app_meta_data`) | gravado pelo backend/admin | **SIM** |
|
|
130
|
+
| `user_metadata` (`raw_user_meta_data`) | **editável pelo próprio usuário** | **NÃO — nunca** |
|
|
131
|
+
|
|
132
|
+
Usar `user_metadata` para autorização (ex.: `role`, `is_admin`, `tenant_id`) é uma vulnerabilidade: o
|
|
133
|
+
usuário muda o próprio claim e escala privilégio. Use sempre `app_metadata`:
|
|
134
|
+
|
|
135
|
+
```sql
|
|
136
|
+
-- pertencimento a time via app_metadata (seguro)
|
|
137
|
+
create policy "User is in team"
|
|
138
|
+
on public.my_table for select
|
|
139
|
+
to authenticated
|
|
140
|
+
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams') );
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Enforçar MFA (assurance level) com policy **restritiva** (restritiva = AND com as permissivas):
|
|
144
|
+
|
|
145
|
+
```sql
|
|
146
|
+
create policy "Restrict updates to MFA users."
|
|
147
|
+
on public.profiles
|
|
148
|
+
as restrictive
|
|
149
|
+
for update
|
|
150
|
+
to authenticated
|
|
151
|
+
using ( (select auth.jwt()->>'aal') = 'aal2' );
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Prefira `permissive` (default, combinam por OR) na maioria dos casos; use `restrictive` só quando
|
|
155
|
+
precisa de uma condição que se soma a **todas** as outras (como o gate de MFA acima).
|
|
156
|
+
|
|
157
|
+
### 5. Multi-tenant: `tenant_id` na policy + índice obrigatório
|
|
158
|
+
|
|
159
|
+
Isolamento de tenant tem que viver na policy, com o `tenant_id` derivado do JWT (via `app_metadata`),
|
|
160
|
+
não passado pelo cliente:
|
|
161
|
+
|
|
162
|
+
```sql
|
|
163
|
+
-- tenant_id vem do app_metadata; cliente não consegue forjar
|
|
164
|
+
create policy "Tenant isolation - select"
|
|
165
|
+
on public.invoices for select
|
|
166
|
+
to authenticated
|
|
167
|
+
using ( tenant_id = ((select auth.jwt()) -> 'app_metadata' ->> 'tenant_id')::uuid );
|
|
168
|
+
|
|
169
|
+
-- ÍNDICE é parte da policy, não opcional
|
|
170
|
+
create index idx_invoices_tenant_id on public.invoices using btree (tenant_id);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Toda coluna comparada na policy (`user_id`, `tenant_id`) precisa de índice. Benchmark da doc: índice em
|
|
174
|
+
`user_id` numa tabela de 100k linhas leva a query de **171ms para <0,1ms** (>100x).
|
|
175
|
+
|
|
176
|
+
### 6. A armadilha de performance nº 1: envolva funções em `(select …)`
|
|
177
|
+
|
|
178
|
+
Chamar `auth.uid()`, `auth.jwt()` ou qualquer função direto na policy faz o Postgres reavaliá-la **por
|
|
179
|
+
linha**. Envolver em `(select …)` dispara um **initPlan** que **cacheia o resultado uma vez por
|
|
180
|
+
statement** (válido porque o resultado não muda entre linhas):
|
|
181
|
+
|
|
182
|
+
```sql
|
|
183
|
+
-- LENTO: auth.uid() reavaliado por linha
|
|
184
|
+
using ( auth.uid() = user_id )
|
|
185
|
+
|
|
186
|
+
-- RÁPIDO: (select …) cacheia via initPlan
|
|
187
|
+
using ( (select auth.uid()) = user_id )
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Vale para funções suas também:
|
|
191
|
+
|
|
192
|
+
```sql
|
|
193
|
+
-- de:
|
|
194
|
+
using ( is_admin() or auth.uid() = user_id )
|
|
195
|
+
-- para:
|
|
196
|
+
using ( (select is_admin()) or (select auth.uid()) = user_id )
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Números reais da doc (tabela 100k linhas, antes → depois):
|
|
200
|
+
|
|
201
|
+
| Cenário | Antes | Depois |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `auth.uid()=user_id` → `(select auth.uid())` | 179ms | 9ms |
|
|
204
|
+
| `is_admin()` com join → `(select is_admin())` | 11.000ms | 7ms |
|
|
205
|
+
| `has_role()=role` → `(select has_role())` | 178.000ms | 12ms |
|
|
206
|
+
| `team_id = any(...)` → versão envolvida | 173.000ms | 16ms |
|
|
207
|
+
|
|
208
|
+
### 7. SECURITY DEFINER para quebrar recursão e otimizar join
|
|
209
|
+
|
|
210
|
+
Quando a policy precisa consultar **outra tabela** que **também tem RLS** (ex.: tabela de papéis/times),
|
|
211
|
+
você cai em recursão ou em joins caros. A solução é uma função `security definer` — ela roda com os
|
|
212
|
+
privilégios de quem **definiu** (dono), **ignorando RLS** dentro dela, então a policy a chama sem
|
|
213
|
+
recursão:
|
|
214
|
+
|
|
215
|
+
```sql
|
|
216
|
+
create or replace function public.user_teams()
|
|
217
|
+
returns int[] as $$
|
|
218
|
+
begin
|
|
219
|
+
return array( select team_id from team_user where auth.uid() = user_id );
|
|
220
|
+
end;
|
|
221
|
+
$$ language plpgsql security definer;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Na policy, envolva o retorno em `array(select …)` para o cache do initPlan:
|
|
225
|
+
|
|
226
|
+
```sql
|
|
227
|
+
-- 1M linhas: =any(user_teams()) sem cache → timeout (>120s)
|
|
228
|
+
-- com array(select …) → 170ms; com índice → 2ms
|
|
229
|
+
using ( team_id = any( array(select public.user_teams()) ) );
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> `security definer` = roda como o dono (ignora RLS interno). `security invoker` = roda como quem chama
|
|
233
|
+
> (respeita RLS). Funções de autorização usadas em policies são `security definer` **de propósito** —
|
|
234
|
+
> mas blinde-as: `search_path` fixo e lógica mínima, porque elas furam o RLS por design.
|
|
235
|
+
|
|
236
|
+
Para **views** (Postgres 15+), o default seguro é `security_invoker` para a view respeitar a RLS das
|
|
237
|
+
tabelas-base:
|
|
238
|
+
|
|
239
|
+
```sql
|
|
240
|
+
create view public.my_view
|
|
241
|
+
with (security_invoker = true)
|
|
242
|
+
as select ... ;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 8. Inverta o join em vez de fazer a tabela-fonte cruzar a alvo
|
|
246
|
+
|
|
247
|
+
Joins na policy que partem da tabela protegida para a tabela de associação são caros. Inverta: filtre na
|
|
248
|
+
tabela de associação e compare a coluna local.
|
|
249
|
+
|
|
250
|
+
```sql
|
|
251
|
+
-- LENTO (9.000ms): join source → target
|
|
252
|
+
using ( auth.uid() in (select user_id from team_user where team_user.team_id = invoices.team_id) )
|
|
253
|
+
|
|
254
|
+
-- RÁPIDO (20ms): inverte — filtra na associação, compara coluna local
|
|
255
|
+
using ( team_id in (select team_id from team_user where user_id = (select auth.uid())) )
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 9. Nunca confie no filtro do cliente — mas adicione-o por performance
|
|
259
|
+
|
|
260
|
+
O `.eq('user_id', userId)` no cliente **não é segurança** (RLS é). Mas adicioná-lo ajuda o planner:
|
|
261
|
+
combinar o filtro explícito com a RLS levou a query de **171ms para 9ms** na doc, porque o índice é
|
|
262
|
+
usado melhor. Regra: RLS garante a segurança; o `.eq()` é otimização redundante, não substituto.
|
|
263
|
+
|
|
264
|
+
### 10. Bypass consciente: `service_role` e `bypassrls`
|
|
265
|
+
|
|
266
|
+
A `service_role` key e roles com `bypassrls` **ignoram toda RLS**. Use no servidor (jobs, webhooks,
|
|
267
|
+
migrações), **jamais no browser**. Expor a `service_role` no front é equivalente a desligar a RLS.
|
|
268
|
+
|
|
269
|
+
```sql
|
|
270
|
+
alter role "background_worker" with bypassrls;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Checklist
|
|
274
|
+
|
|
275
|
+
Antes de marcar uma tabela como segura — qualquer "não" é um vazamento ou um gargalo:
|
|
276
|
+
|
|
277
|
+
- [ ] `enable row level security` está ligado em **toda** tabela do schema `public`?
|
|
278
|
+
- [ ] Existe uma policy por operação (`select`/`insert`/`update`/`delete`) onde aquela operação é
|
|
279
|
+
permitida — e não só `select`?
|
|
280
|
+
- [ ] `insert`/`update` usam `with check` (não só `using`)?
|
|
281
|
+
- [ ] `update` tem uma policy de `select` correspondente?
|
|
282
|
+
- [ ] Toda policy especifica `to authenticated` (ou o role correto), nunca implícito?
|
|
283
|
+
- [ ] Autorização usa `app_metadata` / `auth.uid()` — **nunca** `user_metadata`?
|
|
284
|
+
- [ ] Toda chamada de `auth.uid()`/`auth.jwt()`/função está envolvida em `(select …)`?
|
|
285
|
+
- [ ] Toda coluna comparada na policy (`user_id`, `tenant_id`) tem **índice** btree?
|
|
286
|
+
- [ ] Multi-tenant: `tenant_id` vem do JWT (`app_metadata`), não do payload do cliente?
|
|
287
|
+
- [ ] Consultas a outras tabelas com RLS usam `security definer` (com `search_path` fixo) para evitar
|
|
288
|
+
recursão?
|
|
289
|
+
- [ ] Views usam `with (security_invoker = true)` (PG 15+)?
|
|
290
|
+
- [ ] A `service_role` key existe **apenas** no servidor, nunca no bundle do cliente?
|
|
291
|
+
|
|
292
|
+
## Tabela de decisão "use X quando Y"
|
|
293
|
+
|
|
294
|
+
| Use… | Quando… |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `using` | A condição decide quais **linhas existentes** ler/afetar (`select`, `delete`, lado-read do `update`) |
|
|
297
|
+
| `with check` | A condição valida **linhas novas/modificadas** sendo gravadas (`insert`, lado-write do `update`) |
|
|
298
|
+
| `(select auth.uid())` | **Sempre** — em qualquer policy que chame função; cacheia via initPlan (per-statement, não per-row) |
|
|
299
|
+
| `auth.uid() = user_id` | Isolamento simples por dono da linha (uma tabela, sem times/tenants) |
|
|
300
|
+
| `app_metadata` no `auth.jwt()` | Autorização por role/tenant/time — claim controlado pelo backend, não forjável |
|
|
301
|
+
| `user_metadata` | **Nunca** em autorização (usuário edita); só para preferências de UI sem efeito de segurança |
|
|
302
|
+
| Policy `permissive` (default) | Caso geral — múltiplas policies combinam por **OR** (acesso se qualquer uma passar) |
|
|
303
|
+
| Policy `restrictive` | Gate que deve valer junto com **todas** as outras por **AND** (ex.: exigir `aal2`/MFA) |
|
|
304
|
+
| `to authenticated` | Sempre que a regra só vale para logados — evita rodar predicado caro para `anon` |
|
|
305
|
+
| Índice btree na coluna | **Sempre** que a coluna aparece na policy (`user_id`, `tenant_id`) — ganho >100x |
|
|
306
|
+
| `security definer` | A policy consulta **outra tabela com RLS** (papéis/times/tenants) e há risco de recursão/join caro |
|
|
307
|
+
| `security invoker` (default em funções) | A função deve **respeitar** a RLS de quem a chama — e em **views** (`security_invoker = true`) |
|
|
308
|
+
| `array(select user_teams())` | Função retorna conjunto usado com `= any(...)` — força cache e evita timeout em tabelas grandes |
|
|
309
|
+
| Inverter o join (filtrar na tabela de associação) | A policy faria a tabela protegida cruzar (`join`) a tabela de associação — inverta para usar índice |
|
|
310
|
+
| `service_role` / `bypassrls` | Operação de servidor confiável (jobs, webhooks, migração) — **nunca** exposto ao cliente |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
**Fonte:** Supabase Docs — [Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security),
|
|
315
|
+
[RLS Performance and Best Practices](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv),
|
|
316
|
+
[AI Prompt: Create RLS policies](https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies).
|