# Per-Tenant Subscriptions — Implementation Plan

**Status:** ✅ **ALL STEPS DONE (app 0.2.149, 2026-06-04).** Per-tenant
subscriptions are live: every Pro/Max user gets their own subscriptions +
auto-queue, processed under their own account. The gate is flipped to
tier-based. Offline-verified (99 server tests incl. the ephemeral-session
mechanism + both-mode boot smokes); the actual processing-as-owner run is
the on-device test (see checklist below).

**Step 4 (the hard part), as built:** the background processor now finds
approved items across every scope (`listAutoQueueScopes`) and processes each
AS its owner — `processItemInternally(item, scope)` mints a short-lived real
session (`mintInternalSession` in auth-routes.js) for the owning user, sends
it as the `recap_session` cookie on the loopback `/api/process` call, and
deletes it on every exit path. No auth bypass — a bad/expired token just
401s and the item is marked failed. Single mode sends no cookie (resolves to
"owner"). `userIdForScope`: single→null; multi "owner"→admin; tenant→the
user id. Bonus: this also fixes the operator's OWN multi-mode auto-processing,
which previously ran the loopback with no identity.

**Gate flip:** `PRO_FEATURE_GATES` subscriptions gate is now tier-based in
multi mode (Pro/Max/admin pass, free → 402); frontend `canUseSubscriptions()
= hasEntitlement("subscriptions")` (operator-only clauses reverted).

## Landed in 0.2.147
- `server/subscriptions.js` — scope-keyed storage for subscriptions / skip /
  seen / auto-queue + file-locked `mutateAutoQueue` (atomic read-modify-write,
  replacing the global in-memory `autoQueue`), `listSubscriptionScopes()`,
  `migrateGlobalSubscriptionsToOwner()`, plus the dedup (`getProcessedVideoIds`,
  `isKnownVideo`).
- `index.js` — the check loop fans out over `listSubscriptionScopes()` into a
  per-scope `checkScopeSubscriptions(scope)`; every endpoint resolves
  `scope = subScope(req)` (= scopeForRequest, "owner" for the operator); the
  processor + boot recovery use `mutateAutoQueue`; boot runs the migration +
  per-scope library reconcile. Behind the gate every scope resolves to
  "owner", so behaviour is unchanged for the operator — the plumbing is just
  per-scope now.
- `history.js` — `addToSkipList(scope, videoId)` is scope-keyed.

---

## Goal

Each signed-in Pro/Max tenant manages their **own** channel/podcast
subscriptions; discovered episodes land in **their** auto-queue; approving
one summarizes it under **their** account (their credits, their library,
their relay identity). The operator (admin) keeps theirs. No tenant sees or
affects another's.

## What already exists (foundation)

- `server/subscriptions.js` — extracted, unit-tested. `getProcessedVideoIds(scope)`
  (scope-aware library scan — the dedup fix) + `isKnownVideo()` (pure
  predicate). Already takes a `scope`, so per-tenant dedup is free.
- `history.js` — `scopeForRequest(req)` (→ "owner" for admin/single, else
  `safeComponent(user.id)`), `getScopeHistoryDir(scope)`, `scopeDir`,
  `renameScopeDir`. The whole per-scope filesystem layer is in place.
- The interim isolation gate: `PRO_FEATURE_GATES[subscriptions].adminOnlyInMulti`
  (server) + `canUseSubscriptions()` (frontend). Both get relaxed here.

## The work

### 1. Scope the storage (mechanical, testable)
Move the four global files into `subscriptions.js`, each keyed by scope and
rooted at `scopeDir(scope)` instead of the history root:
`subscriptions.json`, `auto-queue.json`, `skip-list.json`, `seen-list.json`.
Add `loadSubscriptions(scope)` / `saveSubscriptions(scope, …)` + the skip /
seen / auto-queue equivalents. **Drop the global in-memory `autoQueue`** —
load/save per scope per request (the in-memory cache is what makes the
current code single-tenant). Unit-test the round-trips per scope.

### 2. Rescope the endpoints (mechanical)
All ~15 `/api/subscriptions*` + `/api/auto-queue*` handlers derive
`scope = scopeForRequest(req)` and read/write that scope's files. Relax the
gate: drop `adminOnlyInMulti`; gate on the `subscriptions` entitlement
(tier) instead, so paid tenants get in. Update `canUseSubscriptions()` to
`hasEntitlement("subscriptions")` (drop the `!isMulti||isAdmin` clause) and
revert the operator-only frontend branches.

### 3. Rescope the check loop (moderate)
`_checkSubscriptionsInner()` becomes per-scope. Enumerate scopes that have a
subscriptions file (readdir `history/`, keep subdirs whose `subscriptions.json`
is non-empty; plus "owner"). For each: load that scope's subs, dedup via
`getProcessedVideoIds(scope)` + the scope's skip/seen/queue, append to that
scope's auto-queue. The boot + hourly timers iterate all scopes.

### 4. Fix the processor identity (the risky bit — needs on-device test)
`backgroundProcessor()` + `processItemInternally(item)` must process each
item **as its owner**. The item already can carry `scope`/`ownerUserId`.

**Recommended: ephemeral session.** Before the loopback request, mint a
short-lived row in the existing `sessions` table for the owner, send its
token as the `Cookie`, then delete the row in a `finally`. tenant-auth then
resolves `req.user` = owner normally → correct scope, credits, relay
identity. Reuses real auth (no new trust path). **Failure mode is safe:** a
bad/missing session just makes the internal request 401 → the item is
marked `failed`, never an auth hole. Single mode unchanged (no auth, scope
already "owner").

*Alternative:* extract the core of the `/api/process` handler into a
function callable in-process with an explicit `{scope, identity}` (no HTTP,
no cookie). Cleaner long-term but a large refactor of a big handler — more
risk of behavioral drift. Prefer the ephemeral session first.

The single global processor loop stays fine — it just pulls approved items
across all scopes (each item knows its owner) and processes sequentially
with the existing inter-item delay.

### 5. Migration (one-time, on boot)
Move the existing history-root files
(`subscriptions.json`/`auto-queue.json`/`skip-list.json`/`seen-list.json`)
into `history/owner/` (the admin's scope) once, if present and the target
doesn't exist. Preserves the operator's current subscriptions under their
own scope. Idempotent.

## On-device test checklist (gates "done")
1. Tenant A subscribes to a channel; tenant B sees **nothing** of A's.
2. A's discovered episode appears only in A's queue; approving it
   summarizes under **A** (A's credits decremented, saved to A's library,
   A's relay `user:<id>` pool billed).
3. Operator's own subscriptions still discover + auto-process.
4. Per-scope dedup: a video already in A's library is not re-queued for A;
   the same video can still be independently queued for B.
5. Crash-recovery: items stuck `processing` resume under the right owner.
6. Single mode unaffected.

## Effort / risk
Steps 1–3 + 5: ~half a day, low risk, unit-testable offline. Step 4: the
real work — small code, but auth/billing-adjacent, so it carries the
testing burden. Land 1–3 behind the existing operator-only gate first
(no behavior change for tenants), verify, then flip the gate + ship 4.
