c09b9ab7fd
Three seed ADRs to the adr-kit 7-section template: ADR-000 (process + docs/adr vs registry vs docs/architecture boundary), ADR-001 (Vue 3 + Vuetify 3 stack, with an Enforcement block forbidding Inertia/React/framer-motion/Tailwind imports), ADR-002 (PostgreSQL RLS multi-tenancy, documentation-only). adr-lint: 3/3 PASS strictly (completeness + consistency). markdownlint 0 errors. .claude/adr-kit-guide.md vendored from the plugin (replaces what adr-kit:init would write to CLAUDE.md — AK2). cspell glossary += ADR/rvdbreemen/secondsky/NNN/MMM. init/install-hooks NOT run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
2.4 KiB
Markdown
65 lines
2.4 KiB
Markdown
# ADR-002 Isolate tenants with PostgreSQL Row-Level Security
|
|
|
|
## Status
|
|
|
|
Accepted, 2026-05-17.
|
|
|
|
## Context
|
|
|
|
Лидерра is a multi-tenant SaaS CRM: many tenants share one PostgreSQL
|
|
database. Tenant isolation is a security boundary — a cross-tenant data leak
|
|
is a reportable personal-data incident under 152-ФЗ. The boundary has to hold
|
|
even when application code has a bug.
|
|
|
|
This decision predates the ADR process and is recorded retroactively (see
|
|
ADR-000) because it is the load-bearing data-layer decision the rest of the
|
|
backend depends on.
|
|
|
|
## Decision
|
|
|
|
Every tenant-scoped table carries PostgreSQL Row-Level Security policies. The
|
|
current tenant identifier is set per transaction via
|
|
`SET LOCAL app.current_tenant_id` from the `SetTenantContext` middleware. The
|
|
database defines five roles; `crm_supplier_worker` holds `BYPASSRLS`, so any
|
|
queued job running as that role must filter `tenant_id` explicitly in its
|
|
queries — Row-Level Security will not do it for that role.
|
|
|
|
## Alternatives Considered
|
|
|
|
- **Application-layer scoping only (global Eloquent query scopes).** Rejected:
|
|
a single forgotten scope, a raw query, or a new developer unaware of the
|
|
convention leaks cross-tenant rows; defense in depth requires the isolation
|
|
to live in the database.
|
|
- **One database per tenant.** Rejected: the operational cost grows linearly
|
|
with tenant count — every migration runs N times, every backup multiplies —
|
|
and the project specification targets shared-schema multi-tenancy.
|
|
|
|
## Consequences
|
|
|
|
**Positive:**
|
|
|
|
- A cross-tenant read or write is blocked by the database even when the
|
|
application layer has a bug, a missing scope, or a raw query.
|
|
- The isolation rule is auditable in one place — the policy set in
|
|
`db/schema.sql`.
|
|
|
|
**Negative:**
|
|
|
|
- Every new tenant-scoped table must ship an RLS policy plus a
|
|
`db/CHANGELOG_schema.md` entry; omitting the policy is a silent security
|
|
hole until it is caught.
|
|
- `BYPASSRLS` roles such as `crm_supplier_worker` need careful review — code
|
|
running as them carries the isolation responsibility the database otherwise
|
|
enforces.
|
|
|
|
## Related Decisions
|
|
|
|
- ADR-000 — defines the ADR process under which this record was written.
|
|
|
|
## References
|
|
|
|
- `db/schema.sql` — the RLS policy set and role definitions.
|
|
- `db/00_create_roles.sql` — the five production database roles.
|
|
- `app/` Laravel middleware `SetTenantContext` — sets the per-request tenant.
|
|
- `app/` feature test `RlsSmokeTest` — the RLS isolation smoke test.
|