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
|
||||
|
||||
Approaches considered during the 2026-06-02 planning + grill session and **deliberately not
|
||||
|
||||
Reference in New Issue
Block a user