Files
portal/docs/adr/ADR-002-multitenancy-postgres-rls.md
T
Дмитрий c09b9ab7fd feat(adr): bootstrap docs/adr — ADR-000/001/002 + adr-kit guide (Task 4)
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>
2026-05-17 04:54:43 +03:00

2.4 KiB

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.
  • 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.