From 1018dc0df987c2ddeea8f17db5a063dbe8dfdf99 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 22:18:23 +0200 Subject: [PATCH] docs(architecture): document the tapir serve web surface (ADR-011/012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C4 view predated the web transport. Added a "Web surface" section with a component diagram and prose covering: tapir serve (HTMX+Templ over the unchanged store), oidc authenticate-only session, registration gate (users + user_identities), per-user YouTube web connect callback, account disconnect/delete (ADR-013 tapir-side only), immediate summarization via background goroutine + HTMX status poll, and auto/manual summarize mode (migration 006). Relabeled the L2 http node to tapir serve. Corrected the out-of-scope isolation bullet: RLS is live (ADR-012, migration 003), not deferred. ADR-003 stance preserved — the engine/ports/sinks core is untouched; the web is a new transport. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/architecture/architecture.md | 68 +++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index 022f64e..e4ba02d 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -45,7 +45,7 @@ adapter behind an interface (Clean Architecture ports & adapters). ```mermaid graph TB subgraph tapir["Tapir (Go)"] - http["HTTP server
OAuth callbacks +
user-facing API"] + http["tapir serve
(HTMX+Templ web surface:
read summaries, connect,
account, summarize)"] watcher["Watcher
detects new videos
(WebSub + poll)"] engine["Summarization engine
(use-case core)"] resolver["Transcript resolver
(captions-first)"] @@ -95,6 +95,66 @@ two codebases (ADR-003). --- +## Web surface — `tapir serve` (Stage 1, ADR-011 → ADR-012) + +A later transport added over the **unchanged** engine/ports/sinks core (ADR-003): `tapir serve` +is an HTMX+Templ reader/writer (`internal/web`) over the existing `store`. It added no business +logic to the engine — it reads the store and, for one action, kicks the existing engine. ADR-011 +shipped it single-user; ADR-012 opened multi-user with DB-enforced (RLS) isolation. + +```mermaid +graph TB + browser["Browser
(Dex-authenticated user)"] + subgraph web["internal/web (tapir serve)"] + oidc["oidc
Dex OIDC session
(authenticate-only)"] + gate["registration gate
new subject -> /register"] + pages["summary list + detail
(read) + actions"] + connect["/oauth/youtube/callback
per-user token connect"] + account["account
(disconnect, delete)"] + summarize["Summarize button
-> background goroutine"] + end + store[("store
(Postgres, RLS per user)")] + engine["Summarization engine
(unchanged core)"] + secrets["SecretStore
(per-user token refs)"] + + browser --> oidc + oidc --> gate + gate --> pages + pages --> store + connect --> secrets + connect --> store + account --> store + account --> secrets + summarize -->|background| engine + summarize -->|HTMX status poll| store + engine --> store +``` + +- **Dex OIDC session layer** (`internal/web/oidc`) — **authenticate-only** (ADR-012). It proves + *who*; authorization/isolation is the DB's job (RLS), not the session's. +- **Registration gate** — a Dex subject with no `users` row is routed to `/register`, which + creates the `users` row + the `user_identities` mapping (migration 004). Returning subjects + pass straight through. +- **Web-initiated YouTube connect** — `/oauth/youtube/connect` → `/oauth/youtube/callback` + persists a **per-user** refresh-token ref (`youtube//refresh_token`) via `SecretStore` + and a `video_connections` row (ADR-006, migration 005). Distinct from the CLI `tapir auth`. +- **Account management** — `/account` offers disconnect and **delete account**. Delete removes + only Tapir-side state (cascade across the user's tables + secret refs); the shared Dex identity + is left intact (ADR-013). +- **Immediate summarization** — the web "Summarize" button (`POST /v/{id}/summarize`) fires the + engine in a **background goroutine** inside `serve`; the page HTMX-polls `/v/{id}/status`, + showing a Charmbracelet spinner while in-flight (and an honest "queued/waiting" state under + rate-limiting — ADR-014). +- **Summarization mode** — `users.auto_summarize` (migration 006). Auto: every new video is + summarized. Manual (default): new videos appear unsummarized; the button sets + `videos.summarize_requested`, which the next `tapir run` processes and clears. Both the click + path and the batch `tapir run` drive the same unchanged engine. + +The engine, ports, and sink adapters are **untouched** by all of the above — the web surface only +reads the store and triggers the existing engine. Adding it changed wiring, not the core (ADR-003). + +--- + ## Sequence — core use case: new video summarized ```mermaid @@ -191,6 +251,8 @@ Gherkin features in `docs/use-cases/`). - Audio-download + speech-to-text resolver (ADR-007) — would be an additional `VideoSource` fallback path, drawn when built. -- Multi-tenant isolation primitives (per-tenant Postgres role, NetworkPolicy, tenant label) - — activate at Stage 1 (ADR-002); single-user Stage 0 doesn't exercise them. +- Per-user isolation is **live, not deferred**: Postgres RLS `FORCE`d on every user-owned table + (ADR-012, migration 003), realising ADR-002's per-tenant intent at the DB layer. The coarser + multi-tenant primitives (per-namespace NetworkPolicy, Kyverno, tenant label) remain a + Stage-2 hardening item, not exercised yet. - Public SaaS surface (sign-up, billing) — Future C, not built (ADR-008).