Give every Bookmark an owner #22

Closed
opened 2026-08-08 06:06:27 +07:00 by sulthan · 1 comment
Owner

Parent

#18

What to build

Rows stop being anonymous. A Reader table appears, every Bookmark belongs to one, and the owner is seeded as the first and only Reader with all existing rows attached.

Authentication does not change in this ticket — the global token and web password still work exactly as before. Nothing observable changes; the point is that ownership now exists underneath.

Acceptance criteria

  • A Reader table exists, keyed by Discord user ID, carrying a hashed userscript token and a creation time.
  • A Bookmark is keyed by Reader plus site plus series ID. A duplicate Bookmark for the same Reader and Series is impossible at the database level, not merely prevented in code.
  • There is no surrogate id on Bookmarks: nothing references one, so a surrogate would add a permanently unused index plus a second unique index enforcing what the composite key already enforces.
  • Bookmarks reference Series by foreign key. Deleting a Reader cascades to their Bookmarks.
  • Startup seeds exactly one Reader from the configured owner Discord ID, idempotently.
  • Existing Bookmarks are attached to that Reader by a run-once migration that the version table prevents applying twice.
  • Every store read and write is scoped to a Reader.
  • Existing authentication is untouched and no behaviour change is observable from outside.

Blocked by

#21

## Parent #18 ## What to build Rows stop being anonymous. A Reader table appears, every Bookmark belongs to one, and the owner is seeded as the first and only Reader with all existing rows attached. Authentication does not change in this ticket — the global token and web password still work exactly as before. Nothing observable changes; the point is that ownership now exists underneath. ## Acceptance criteria - [x] A Reader table exists, keyed by Discord user ID, carrying a hashed userscript token and a creation time. - [x] A Bookmark is keyed by Reader plus site plus series ID. A duplicate Bookmark for the same Reader and Series is impossible at the database level, not merely prevented in code. - [x] There is no surrogate id on Bookmarks: nothing references one, so a surrogate would add a permanently unused index plus a second unique index enforcing what the composite key already enforces. - [x] Bookmarks reference Series by foreign key. Deleting a Reader cascades to their Bookmarks. - [x] Startup seeds exactly one Reader from the configured owner Discord ID, idempotently. - [x] Existing Bookmarks are attached to that Reader by a run-once migration that the version table prevents applying twice. - [x] Every store read and write is scoped to a Reader. - [x] Existing authentication is untouched and no behaviour change is observable from outside. ## Blocked by #21
sulthan added the ready-for-agent label 2026-08-08 06:06:27 +07:00
Author
Owner

Implemented on branch feat/reader-ownership (commit 3f7664e).

  • Migration 0003: readers table (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.
  • 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.
  • Store: List/Get/Upsert/Delete take readerID; wire key derived as site:series_id on read. Handlers act as Store.OwnerID() while the global token remains the only credential — auth and wire format untouched.
  • Series-level methods (due queue, mark-checked, set-latest-chapter) deliberately unscoped: shared rows, poller aggregates across readers (ADR-0003).
  • New env OWNER_DISCORD_ID (required); compose, .env.example, DEPLOY.md, README.md updated.
  • Tests: seed idempotency + hash refresh, 0004 attach migration, DB-level duplicate impossibility, per-reader scoping, cascade delete; full suite green (go test ./...), plus a live smoke test (fresh Postgres: seed → PUT/GET → restart idempotent).
Implemented on branch `feat/reader-ownership` (commit 3f7664e). - Migration 0003: `readers` table (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`. - 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. - Store: `List/Get/Upsert/Delete` take `readerID`; wire `key` derived as `site:series_id` on read. Handlers act as `Store.OwnerID()` while the global token remains the only credential — auth and wire format untouched. - Series-level methods (due queue, mark-checked, set-latest-chapter) deliberately unscoped: shared rows, poller aggregates across readers (ADR-0003). - New env `OWNER_DISCORD_ID` (required); compose, .env.example, DEPLOY.md, README.md updated. - Tests: seed idempotency + hash refresh, 0004 attach migration, DB-level duplicate impossibility, per-reader scoping, cascade delete; full suite green (`go test ./...`), plus a live smoke test (fresh Postgres: seed → PUT/GET → restart idempotent).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#22