From aa3f1631a63941a815a232d0ca4da210a405bb34 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 15:06:23 +0200 Subject: [PATCH] =?UTF-8?q?docs(adr):=20ADR-012=20=E2=80=94=20open=20Stage?= =?UTF-8?q?=201=20(multi-user)=20with=20enforced=20RLS=20isolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- DECISIONS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/DECISIONS.md b/DECISIONS.md index 8e54ecf..6544d09 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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