docs(architecture): document the tapir serve web surface (ADR-011/012)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 22:18:23 +02:00
co-authored by Claude Opus 4.8
parent 74f4fd7f2a
commit 1018dc0df9
+65 -3
View File
@@ -45,7 +45,7 @@ adapter behind an interface (Clean Architecture ports & adapters).
```mermaid
graph TB
subgraph tapir["Tapir (Go)"]
http["HTTP server<br/>OAuth callbacks +<br/>user-facing API"]
http["tapir serve<br/>(HTMX+Templ web surface:<br/>read summaries, connect,<br/>account, summarize)"]
watcher["Watcher<br/>detects new videos<br/>(WebSub + poll)"]
engine["Summarization engine<br/>(use-case core)"]
resolver["Transcript resolver<br/>(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<br/>(Dex-authenticated user)"]
subgraph web["internal/web (tapir serve)"]
oidc["oidc<br/>Dex OIDC session<br/>(authenticate-only)"]
gate["registration gate<br/>new subject -> /register"]
pages["summary list + detail<br/>(read) + actions"]
connect["/oauth/youtube/callback<br/>per-user token connect"]
account["account<br/>(disconnect, delete)"]
summarize["Summarize button<br/>-> background goroutine"]
end
store[("store<br/>(Postgres, RLS per user)")]
engine["Summarization engine<br/>(unchanged core)"]
secrets["SecretStore<br/>(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/<userID>/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).