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:
2026-06-03 15:06:23 +02:00
parent 598ba5d34f
commit 7288f29cc6
+37
View File
@@ -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