docs(adr): ADR-012 — open Stage 1 (multi-user) with enforced RLS isolation
Maintainer's call to open Stage 1 ahead of the Stage-0 gate. Non-negotiable: multi-user ships WITH DB-enforced isolation (Postgres RLS, FORCE'd on the owner role, per-request tapir.current_user_id) and a passing two-user isolation test in the same slice — the VISION Stage-2 bar pulled forward, not deferred. Replaces ADR-011's allowlist-of-one with per-subject users rows; adds video_connections + subscriptions.
This commit is contained in:
@@ -253,6 +253,43 @@ Full spec: `docs/ui-spec.md`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ADR-012 — Open Stage 1: multi-user with enforced isolation (RLS) in the same slice
|
||||||
|
|
||||||
|
**Status:** Accepted (2026-06-03)
|
||||||
|
|
||||||
|
**Context.** ADR-011 shipped a single-user web reader with an allowlist of one, deferring
|
||||||
|
multi-user authz/isolation to "a new ADR (Stage 1)". The maintainer has chosen to **open
|
||||||
|
Stage 1 now** — multi-user registration, per-user YouTube connect, and account management —
|
||||||
|
ahead of the formal Stage-0 self-use gate. VISION's hard invariant ("data isolation is a
|
||||||
|
promise, not a feature flag … holds from the first user") and the drift signal ("building
|
||||||
|
Stage 1+ machinery before the gate") make one thing non-negotiable: **multi-user features
|
||||||
|
must not ship before isolation is enforced.**
|
||||||
|
|
||||||
|
**Decision.**
|
||||||
|
1. **Open Stage 1.** Build registration (explicit, not just-in-time: a Dex-authenticated
|
||||||
|
subject with no `users` row completes a registration step that creates it), per-user
|
||||||
|
web-initiated YouTube OAuth connect (distinct from the CLI `tapir auth`), and account
|
||||||
|
management (view / disconnect / delete).
|
||||||
|
2. **Isolation is DB-enforced via Postgres Row-Level Security**, not application-layer
|
||||||
|
filtering — realising ADR-002's per-tenant-role intent. RLS is `FORCE`d on every
|
||||||
|
user-owned table (the app connects as the non-superuser table-owner `tapir` role, which
|
||||||
|
would otherwise bypass RLS); every request scopes rows via `tapir.current_user_id`
|
||||||
|
(`SET LOCAL` inside a transaction), routed through a single structural helper so scoping
|
||||||
|
is not per-query opt-in.
|
||||||
|
3. **The Stage-2 isolation bar is pulled forward into THIS slice, not deferred.** A real
|
||||||
|
isolation test (two users, disposable/embedded Postgres) — user A reads/writes zero of
|
||||||
|
user B's rows across every table — ships green **with** the multi-user features. No
|
||||||
|
multi-user feature merges ahead of that test passing.
|
||||||
|
|
||||||
|
**Consequences.** Stage 1 + the Stage-2 isolation guarantee land together; isolation is
|
||||||
|
structural (DB), so it cannot be forgotten per-query. Adds `video_connections` and
|
||||||
|
`subscriptions` tables, a web OAuth callback, and a registration surface. The single-user
|
||||||
|
allowlist (ADR-011) is replaced by per-subject `users` rows. Reversible only by a superseding
|
||||||
|
ADR. This deliberately advances ahead of the Stage-0 gate — recorded as the maintainer's
|
||||||
|
explicit call, with isolation as the guardrail that keeps it safe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Rejected alternatives
|
## Rejected alternatives
|
||||||
|
|
||||||
Approaches considered during the 2026-06-02 planning + grill session and **deliberately not
|
Approaches considered during the 2026-06-02 planning + grill session and **deliberately not
|
||||||
|
|||||||
Reference in New Issue
Block a user