Give every Bookmark an owner (Reader table) #30

Merged
sulthan merged 1 commits from feat/reader-ownership into main 2026-08-08 08:05:18 +07:00
Owner

Closes #22

What

A readers table appears; every Bookmark belongs to one. The owner is seeded as the first and only Reader, and all existing rows are attached to them.

  • Migration 0003: readers (discord_id UNIQUE, token_sha256 UNIQUE, created_at).
  • Migration 0004 (run-once, version-table-gated): attaches existing bookmarks to the seeded owner, drops the surrogate key column, composite PK (reader_id, site, series_id), FK to readers ON DELETE CASCADE — a duplicate Bookmark for one Reader and Series is impossible at the database level.
  • Seed: Store.Open runs schema to 0003, seeds exactly one owner row from OWNER_DISCORD_ID (hash = SHA-256 of API_TOKEN, refreshed on every start so rotation stays current), then migrates the rest.
  • Scoping: List/Get/Upsert/Delete take readerID; the wire key is derived as site:series_id on read. Handlers act as Store.OwnerID() while the global token remains the only credential.
  • Unchanged: authentication and the flat wire format — nothing observable changes from outside.
  • New env OWNER_DISCORD_ID (required): compose, .env.example, DEPLOY.md, README.md, backend/AGENTS.md updated.

Series-level methods (due queue, mark-checked, set-latest-chapter) stay unscoped deliberately: series are shared rows polled once per due cycle, and the reader_count ordering requires cross-reader visibility (ADR-0003).

Verification

  • go test ./... green, including new tests: seed idempotency + hash refresh, 0004 attach migration, DB-level duplicate impossibility, per-reader scoping, reader-delete cascade.
  • Live smoke test on fresh Postgres: seed → PUT/GET (flat wire intact) → restart idempotent; stored hash matches SHA-256 of the token.

Deploy note

OWNER_DISCORD_ID is required after this lands — the backend refuses to start without it. Set it to the owner's Discord snowflake (Settings → Advanced → Developer Mode → right-click name → Copy User ID).

Closes #22 ## What A `readers` table appears; every Bookmark belongs to one. The owner is seeded as the first and only Reader, and all existing rows are attached to them. - **Migration 0003**: `readers` (discord_id UNIQUE, token_sha256 UNIQUE, created_at). - **Migration 0004** (run-once, version-table-gated): attaches existing bookmarks to the seeded owner, drops the surrogate `key` column, composite PK `(reader_id, site, series_id)`, FK to readers `ON DELETE CASCADE` — a duplicate Bookmark for one Reader and Series is impossible at the database level. - **Seed**: `Store.Open` runs schema to 0003, seeds exactly one owner row from `OWNER_DISCORD_ID` (hash = SHA-256 of `API_TOKEN`, refreshed on every start so rotation stays current), then migrates the rest. - **Scoping**: `List/Get/Upsert/Delete` take `readerID`; the wire `key` is derived as `site:series_id` on read. Handlers act as `Store.OwnerID()` while the global token remains the only credential. - **Unchanged**: authentication and the flat wire format — nothing observable changes from outside. - **New env** `OWNER_DISCORD_ID` (required): compose, .env.example, DEPLOY.md, README.md, backend/AGENTS.md updated. Series-level methods (due queue, mark-checked, set-latest-chapter) stay unscoped deliberately: series are shared rows polled once per due cycle, and the reader_count ordering requires cross-reader visibility (ADR-0003). ## Verification - `go test ./...` green, including new tests: seed idempotency + hash refresh, 0004 attach migration, DB-level duplicate impossibility, per-reader scoping, reader-delete cascade. - Live smoke test on fresh Postgres: seed → PUT/GET (flat wire intact) → restart idempotent; stored hash matches SHA-256 of the token. ## Deploy note `OWNER_DISCORD_ID` is required after this lands — the backend refuses to start without it. Set it to the owner's Discord snowflake (Settings → Advanced → Developer Mode → right-click name → Copy User ID).
sulthan added 1 commit 2026-08-08 08:03:05 +07:00
A readers table appears, keyed by Discord user ID and carrying the SHA-256
of the owner's userscript token (the global API token today). Startup seeds
exactly one Reader from OWNER_DISCORD_ID, idempotently, and a run-once
migration (0004, version-table-gated) attaches existing bookmarks to it
before reshaping: the surrogate key column is dropped and bookmarks are
keyed (reader_id, site, series_id) with an FK to readers ON DELETE CASCADE,
so a duplicate bookmark for one Reader and Series is impossible at the
database level.

Every store read and write is now scoped to the reader it names; handlers
act as the seeded owner while the global token remains the only credential.
Authentication and the wire format are untouched: the flat JSON still
carries key/site/series_id, with key derived on read.

OWNER_DISCORD_ID is a new required env var (compose + docs updated).
sulthan merged commit 8cebb94b92 into main 2026-08-08 08:05:18 +07:00
Sign in to join this conversation.