docs(data-model): uploaded_transcripts, and fix the stale claim that transcripts is RLS'd
Adds the private uploaded_transcripts entity and the exact-set isolation rule (ADR-033). Also corrects two lists that still named `transcripts` as an RLS'd table: it has been shared non-RLS since ADR-021 / migration 015, and these lists sit next to the very boundary that matters. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+35
-3
@@ -18,7 +18,12 @@ only opaque references to them; the secret material lives in ESO/1Password (ADR-
|
|||||||
not LLM re-summarization but a rate-gated, reputation-risky caption fetch (ADR-010/014), which
|
not LLM re-summarization but a rate-gated, reputation-risky caption fetch (ADR-010/014), which
|
||||||
is paid per re-fetch regardless of user count — so persisting public caption content once and
|
is paid per re-fetch regardless of user count — so persisting public caption content once and
|
||||||
sharing it strictly beats the coupling it removes. Everything else each user owns is
|
sharing it strictly beats the coupling it removes. Everything else each user owns is
|
||||||
self-contained; `rls_test.go` proves transcripts is the single exception.
|
self-contained; `rls_test.go` proves transcripts is the single exception for *content*.
|
||||||
|
**Uploaded-audio transcripts are the opposite of public caption content** (private, owned by
|
||||||
|
one user) and therefore live in their own RLS-scoped `uploaded_transcripts` table, never in
|
||||||
|
`transcripts` (ADR-033). The isolation test now also pins the **exact set** of tables with row
|
||||||
|
security off (`transcripts`, `user_identities`, `invitations`, `schema_migrations`), so a new
|
||||||
|
table added without RLS fails CI.
|
||||||
- **Secrets by reference only.** Tables hold a `secret_ref` (opaque string/UUID resolved via
|
- **Secrets by reference only.** Tables hold a `secret_ref` (opaque string/UUID resolved via
|
||||||
the `SecretStore` port), never tokens or keys.
|
the `SecretStore` port), never tokens or keys.
|
||||||
- **The brain sink is just a delivery target.** No brain-specific tables. Whether a summary
|
- **The brain sink is just a delivery target.** No brain-specific tables. Whether a summary
|
||||||
@@ -41,6 +46,7 @@ erDiagram
|
|||||||
VIDEO ||--o| SUMMARY : "has at most one"
|
VIDEO ||--o| SUMMARY : "has at most one"
|
||||||
SUMMARY ||--o{ SINK_DELIVERY : "delivered via"
|
SUMMARY ||--o{ SINK_DELIVERY : "delivered via"
|
||||||
USER ||--o{ CHANNEL_ERROR : "reports unavailable channels"
|
USER ||--o{ CHANNEL_ERROR : "reports unavailable channels"
|
||||||
|
USER ||--o{ UPLOADED_TRANSCRIPT : "uploads (private, ADR-033)"
|
||||||
|
|
||||||
USER {
|
USER {
|
||||||
uuid id PK
|
uuid id PK
|
||||||
@@ -136,12 +142,27 @@ erDiagram
|
|||||||
timestamptz first_seen
|
timestamptz first_seen
|
||||||
timestamptz last_seen
|
timestamptz last_seen
|
||||||
}
|
}
|
||||||
|
UPLOADED_TRANSCRIPT {
|
||||||
|
uuid id PK
|
||||||
|
uuid user_id FK "-> USER, ON DELETE CASCADE, NOT NULL; RLS ENABLE+FORCE (migration 017)"
|
||||||
|
text title
|
||||||
|
text status "queued | transcribing | done | failed"
|
||||||
|
text content "nullable until done; the transcript text"
|
||||||
|
jsonb segments "nullable until done; [{start,end,text}] in seconds"
|
||||||
|
text endpoint "which backend served it: iguana/... | berget/... (berget = audio left the homelab)"
|
||||||
|
float audio_seconds
|
||||||
|
text error_kind "typed: interrupted | transcription_failed — never a provider string"
|
||||||
|
timestamptz created_at
|
||||||
|
timestamptz finished_at
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`SUMMARY_ACTION` has `UNIQUE (user_id, video_id, action)`; `VIDEO_CONNECTION` has
|
`SUMMARY_ACTION` has `UNIQUE (user_id, video_id, action)`; `VIDEO_CONNECTION` has
|
||||||
`UNIQUE (user_id, provider)` (one connection per provider — reconnect upserts in place).
|
`UNIQUE (user_id, provider)` (one connection per provider — reconnect upserts in place).
|
||||||
RLS (`ENABLE` + `FORCE`) is on **every solid user-owned table above** — `users`, `videos`,
|
RLS (`ENABLE` + `FORCE`) is on **every solid user-owned table above** — `users`, `videos`,
|
||||||
`transcripts`, `summaries`, `summary_actions`, `video_connections`, `channel_errors`.
|
`summaries`, `summary_actions`, `video_connections`, `channel_errors`, `uploaded_transcripts`.
|
||||||
|
(`transcripts` is deliberately **not** in this list: since ADR-021 it is shared public caption
|
||||||
|
content with no `user_id`.)
|
||||||
`sink_deliveries` is RLS'd via an `EXISTS` on its parent summary; `user_identities` is
|
`sink_deliveries` is RLS'd via an `EXISTS` on its parent summary; `user_identities` is
|
||||||
intentionally **not** RLS'd (auth plumbing). See the *Isolation invariant* section for the
|
intentionally **not** RLS'd (auth plumbing). See the *Isolation invariant* section for the
|
||||||
mechanism.
|
mechanism.
|
||||||
@@ -197,6 +218,14 @@ mechanism.
|
|||||||
the most recent discovery pass. Upserted per scheduler pass (`last_seen` refreshed each run);
|
the most recent discovery pass. Upserted per scheduler pass (`last_seen` refreshed each run);
|
||||||
surfaced on the account page as a warning. Cascades on user deletion. Primary key is
|
surfaced on the account page as a warning. Cascades on user deletion. Primary key is
|
||||||
`(user_id, channel_id)`. FORCE RLS'd.
|
`(user_id, channel_id)`. FORCE RLS'd.
|
||||||
|
- **UPLOADED_TRANSCRIPTS** (migration 017, ADR-033) — the **private** transcript of audio a user
|
||||||
|
uploaded. Deliberately a separate table from the shared `transcripts`: that one is non-RLS on the
|
||||||
|
grounds that it holds only public caption content, and a recorded meeting is the opposite.
|
||||||
|
`user_id NOT NULL ... ON DELETE CASCADE`, FORCE RLS'd. `status` runs `queued → transcribing →
|
||||||
|
done | failed`; `done` requires `content`, `failed` requires a typed `error_kind` (CHECK
|
||||||
|
constraints). `endpoint` records which backend served it — `berget/...` means the audio left the
|
||||||
|
homelab. The audio itself is never stored. `segments` keeps the timestamps (re-transcribing 90
|
||||||
|
minutes costs ~7). Unlike `summary_actions`/`login_events` it **does** cascade on user deletion.
|
||||||
- **LOGIN_EVENTS** (migration 010) — throttled one-row-per-(user, date) login stamp. Used by the
|
- **LOGIN_EVENTS** (migration 010) — throttled one-row-per-(user, date) login stamp. Used by the
|
||||||
Stage-0 gate query (VISION §Stage 0: "returned and used in ≥2 distinct weeks").
|
Stage-0 gate query (VISION §Stage 0: "returned and used in ≥2 distinct weeks").
|
||||||
|
|
||||||
@@ -209,7 +238,10 @@ enforcement dormant); **ADR-012 opened Stage 1 and turned enforcement on in the
|
|||||||
Enforcement is **Postgres Row-Level Security** (migration `003_rls.up.sql`):
|
Enforcement is **Postgres Row-Level Security** (migration `003_rls.up.sql`):
|
||||||
|
|
||||||
- RLS is `ENABLE`d **and** `FORCE`d on every user-owned table — `users`, `videos`,
|
- RLS is `ENABLE`d **and** `FORCE`d on every user-owned table — `users`, `videos`,
|
||||||
`transcripts`, `summaries`, `summary_actions`, `video_connections`, `channel_errors`.
|
`summaries`, `summary_actions`, `video_connections`, `channel_errors`, and (migration 017)
|
||||||
|
`uploaded_transcripts`. `transcripts` is **not** one of them: ADR-021 made it shared public
|
||||||
|
content, and migration 003's original per-user `transcripts` policy was dropped with that
|
||||||
|
table in migration 015.
|
||||||
`FORCE` is load-bearing: the app connects as the table **owner** (`tapir` role), and owners
|
`FORCE` is load-bearing: the app connects as the table **owner** (`tapir` role), and owners
|
||||||
bypass RLS unless forced.
|
bypass RLS unless forced.
|
||||||
- Each policy keys off the per-request GUC `tapir.current_user_id`, set transaction-locally by
|
- Each policy keys off the per-request GUC `tapir.current_user_id`, set transaction-locally by
|
||||||
|
|||||||
Reference in New Issue
Block a user