Effort 2: make data ownership per-user (feeds, aggregates, OPML) #65

Closed
opened 2026-10-02 21:07:07 +00:00 by agent-pi · 2 comments
Collaborator

Part of #25 (Phase 6 multi-user). Effort 1 added authentication, sessions, and CSRF, but the data behind the login is still single-owner: there is one global feed list, one global set of articles and aggregates, and OPML import/export and the CLI act on the whole instance. A second account logging in sees and manages the same feeds and topics as the first.

Yunjin committed to fully independent users: each account gets its own feeds, OPML, and aggregates. Making ownership a schema-level concern touches the feed table shape, the fetch worker, the LLM aggregation pipeline (which has a per-run API cost), the OPML flows, and an Alembic migration so existing single-user deployments keep working with their data assigned to the seeded default user.

Design brainstormed and approved 2026-10-03; implementation plan to follow.

Part of #25 (Phase 6 multi-user). Effort 1 added authentication, sessions, and CSRF, but the data behind the login is still single-owner: there is one global feed list, one global set of articles and aggregates, and OPML import/export and the CLI act on the whole instance. A second account logging in sees and manages the same feeds and topics as the first. Yunjin committed to fully independent users: each account gets its own feeds, OPML, and aggregates. Making ownership a schema-level concern touches the feed table shape, the fetch worker, the LLM aggregation pipeline (which has a per-run API cost), the OPML flows, and an Alembic migration so existing single-user deployments keep working with their data assigned to the seeded default user. Design brainstormed and approved 2026-10-03; implementation plan to follow.
Author
Collaborator

Design findings (brainstormed with Marvin8 2026-10-03, spec at .superpowers/specs/2026-10-03-effort2-ownership-design.md, git-ignored):

  • Scale is 2–3 family users; yunjin fetch/aggregate run on a fixed cadence from the container worker entry (container-entrypoint.sh, default hourly).
  • Schema: keep feeds global, add feed_subscriptions(user_id, feed_id) — duplicate subscriptions must not duplicate fetches/full-text/media/storage, and articles.url UNIQUE must survive.
  • LLM cost: per-user clustering with no-op skips — a user is only sent to the LLM when articles are new for them (their last-run watermark, plus a backlog rule for feeds subscribed after it). Cost tracks content, not ticks.
  • Aggregates gain user_id; read-path queries scope to the session user; cross-user aggregate ids 404.
  • OPML import/export scope to the current user; "skipped" = already subscribed by you.
  • Migration: one revision off 20261002_add_user_credentials; everything existing backfills to user 1 (default).

Plan of attack

  1. Alembic migration: feed_subscriptions table + aggregates.user_id NOT NULL (table rebuild), backfill to user 1.
  2. db/subscriptions.py (pure SQL CRUD) + FeedSubscription dataclass; Aggregate.user_id.
  3. services/subscriptions.py: subscribe-or-create-feed rule; unsubscribe flow with per-user aggregate-link cleanup and orphan-feed GC (last subscriber out deletes feed; FK cascades take articles/media/tags/read-state).
  4. Web feeds routes: add/discover/list/unsubscribe scoped to session user; OPML import/export per-user.
  5. LlmAggregationService: per-user loop, watermark + backlog rule, per-user TTL expiry, refresh isolation, failure isolation. fetch_all_feeds needs no change (GC invariant: feeds rows ≡ union of subscriptions).
  6. Reader read-path: aggregates + unread fan-out + article→aggregate lookup scoped to session user; cross-user ids 404.
  7. CLI: --user on add-feed/list-feeds (default default); docs (docs/cli.md, docs/reader.md) in the same PR.

TDD throughout; uv run nox green before every push. Work proceeds on feat/issue-65-data-ownership cut from feat/multiuser; sub-PR targets feat/multiuser (COLLECTING BRANCH), Refs #25.

Design findings (brainstormed with Marvin8 2026-10-03, spec at `.superpowers/specs/2026-10-03-effort2-ownership-design.md`, git-ignored): - Scale is 2–3 family users; `yunjin fetch`/`aggregate` run on a fixed cadence from the container worker entry (`container-entrypoint.sh`, default hourly). - Schema: keep `feeds` global, add `feed_subscriptions(user_id, feed_id)` — duplicate subscriptions must not duplicate fetches/full-text/media/storage, and `articles.url UNIQUE` must survive. - LLM cost: per-user clustering with no-op skips — a user is only sent to the LLM when articles are new *for them* (their last-run watermark, plus a backlog rule for feeds subscribed after it). Cost tracks content, not ticks. - Aggregates gain `user_id`; read-path queries scope to the session user; cross-user aggregate ids 404. - OPML import/export scope to the current user; "skipped" = already subscribed by you. - Migration: one revision off `20261002_add_user_credentials`; everything existing backfills to user 1 (`default`). **Plan of attack** 1. Alembic migration: `feed_subscriptions` table + `aggregates.user_id NOT NULL` (table rebuild), backfill to user 1. 2. `db/subscriptions.py` (pure SQL CRUD) + `FeedSubscription` dataclass; `Aggregate.user_id`. 3. `services/subscriptions.py`: subscribe-or-create-feed rule; unsubscribe flow with per-user aggregate-link cleanup and orphan-feed GC (last subscriber out deletes feed; FK cascades take articles/media/tags/read-state). 4. Web feeds routes: add/discover/list/unsubscribe scoped to session user; OPML import/export per-user. 5. `LlmAggregationService`: per-user loop, watermark + backlog rule, per-user TTL expiry, refresh isolation, failure isolation. `fetch_all_feeds` needs no change (GC invariant: feeds rows ≡ union of subscriptions). 6. Reader read-path: aggregates + unread fan-out + article→aggregate lookup scoped to session user; cross-user ids 404. 7. CLI: `--user` on add-feed/list-feeds (default `default`); docs (`docs/cli.md`, `docs/reader.md`) in the same PR. TDD throughout; `uv run nox` green before every push. Work proceeds on `feat/issue-65-data-ownership` cut from `feat/multiuser`; sub-PR targets `feat/multiuser` (**COLLECTING BRANCH**), Refs #25.
Author
Collaborator

Landed on main in release 2026.10.6.1 (PR #69, merge 89f26f7). Closing.

Landed on `main` in release **2026.10.6.1** (PR #69, merge `89f26f7`). Closing.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/yunjin#65
No description provided.