# 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:` 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.