Files
recap/docs/core-decoupling-plan.md
Keysat 0ae59f3550 Add multi-tenant cloud mode: self-serve purchase, credit metering, core-decoupling
Introduces RECAP_MODE=multi alongside single-mode self-host:
- Tenant auth + accounts (magic-link via System SMTP), per-tenant credit pool,
  anonymous trial minting with per-IP/-64 caps
- Self-serve Pro/Max purchase: inline Lightning (BTCPay) + card (Zaprite),
  prepaid 30-day periods, expiry-reminder emails
- Core-decoupling: relay owns cloud tier/expiry keyed by Recaps user-id
- SQLite (better-sqlite3) schema for multi-mode; filesystem unchanged for single
- StartOS actions/versions through 0.2.155
2026-06-13 14:25:05 -05:00

176 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Core Decoupling — Implementation Plan (relay-owns)
**Status:****Implemented + build-ready, 2026-06-04.** Both sides code-
complete, typecheck/syntax clean, unit tests green. Relay bumped to
`0.2.119`, Recaps app to `0.2.143`. Not yet installed/configured on-device —
see "Install + configure runbook" at the bottom.
Scoped slice of `architecture-simplification-plan.md`, with the May plan's
"Recaps-owns billing" reversed per Grant's decision.
## Decisions locked (2026-06-03)
1. **The Recap Relay owns the Pro/Max subscription**, keyed by the Recaps
**user-id** (not a Keysat license). Recaps reads each user's tier from
the relay to gate features.
2. **No credit-pool migration** — no real customers yet; clean cutover.
3. **Keysat leaves the cloud path entirely.** A cloud user has no license.
Keysat/licenses remain ONLY for the (future) self-hosted-operator case.
4. **Server auth = a shared "operator key."** The `recaps.cc` server proves
itself to the relay with a shared secret; it then vouches for its users
via `X-Recap-User-Id`.
5. **Self-serve subscription purchase is DEFERRED.** For this slice, tiers
are **operator-set** (Grant grants Pro/Max). Self-serve BTCPay/card
subscription buying + expiry + renewal = the later "payment" slice.
## Goal (one sentence)
Replace "every cloud relay request carries the user's Keysat license" with
"the `recaps.cc` server authenticates once with an operator key and passes
the user's account-id; the relay tracks that user's tier + credits."
---
## How it works after the change
```
Cloud user's browser
│ (logged-in Recaps session cookie)
recaps.cc server ──reads user's tier from relay, gates features──┐
│ POST /relay/<...> │
│ X-Recap-User-Id: <recaps user id> │
│ X-Recap-Operator-Key: <shared secret> ← proves it's │
│ (NO per-user license bearer) Grant's server│
▼ │
Recap Relay │
• validates operator key → trusts the user-id │
• credit pool keyed by user:<id> │
• stores that user's TIER (+ optional expiry) on the pool ─────┘
• applies the tier's monthly credit quota
```
Self-hosted operators are unchanged: they send a license bearer and no
`X-Recap-User-Id`, so they keep the existing `lic:`/`inst:` path.
---
## Relay changes (recap-relay/)
1. **Identity resolver** (new helper used by every route in place of
`resolveLicense(auth)` + raw installId): if `X-Recap-User-Id` is present
AND `X-Recap-Operator-Key` matches config → identity =
`{ creditKey: "user:<id>", source: "cloud" }`. Else existing license/
install path (`credits.js` `getCreditKey` / `resolveLicense`).
2. **Tier-of-record on the pool** (`credits.js` ledger row): make `tier`
(+ optional `subscription_expires_at`) an authoritative, persisted field
for `user:` pools, instead of reading tier from a license each request.
Quota math (`getTierQuotas`) keys off it as today.
3. **Operator endpoint to set a user's tier**
`POST /admin/users/:userId/tier { tier, expires_at? }` (admin-auth
gated). This is how tiers get set in this slice; the future self-serve
purchase flow writes the same field.
4. **Report tier + balance for a user-id** — extend `/relay/balance` (and
the status surface) to answer for the `user:<id>` identity so Recaps can
read it.
5. **Config:** `relay_cloud_operator_key` (+ a StartOS action to set it,
Phase 1.5).
## Recaps changes (recap/)
6. **Cloud relay identity** (`providers/index.js` `pickRelayIdentity` +
`providers/relay.js` `buildHeaders`): multi-mode cloud user → send
`X-Recap-User-Id` + `X-Recap-Operator-Key` (from server config), DROP the
user-license bearer. Single-mode / self-hosted unchanged.
7. **Entitlement checks read the relay-reported tier**
(`license-middleware.js` multi-mode branch, `tts-routes.js`
`userHasTtsAccess`): derive tier from what the relay reports for this
user (cached on `req.user` / relay-status), not from a parsed license.
Single-mode keeps using the operator `LIC`.
8. **Stop attaching a Keysat license at cloud signup**
(`license-purchase.js`): cloud accounts no longer get a license. (The
existing flow stays available for the self-hosted operator-license case
only.)
9. **Config:** `recap_relay_operator_key` (server-side; never sent to the
browser).
---
## Explicitly deferred (later "payment" slice)
Self-serve Pro/Max subscription purchase (monthly BTCPay/card, expiry,
renewal emails, cancel) · removing the free signed-in tier · "Take Recaps
home" rework. None of these block the decoupling; tiers are operator-set
until then.
## Testing / rollout
1. Relay: identity resolver — cloud (valid key)→`user:` ; cloud (bad/no
key)→reject/fallback ; self-hosted→`lic:`/`inst:`.
2. Relay: operator-set tier → `/relay/balance` reports it → metered call
decrements the `user:` pool at the right quota.
3. Recaps: feature gates (clips, subscriptions, TTS) follow the relay tier.
4. Ship relay first (accepts both old + new), then Recaps cutover. Verify a
self-hosted-style license request still works. Both via `make install` /
sideload — **no registry deploys.**
## Effort
~**23 focused days** (smaller than the migration-laden version): ~1 on the
relay (resolver, tier-on-pool, operator endpoint, config), ~1 on Recaps
(headers, tier-read, gate rewiring), ~0.5 testing.
## Sequencing (resolved 2026-06-03)
Core decoupling ships first with **operator-set tiers**; self-serve
subscription purchase is the immediate next slice. Rationale: land the
structural de-licensing on its own and verify it, then add money-handling
code on a proven foundation rather than entangling a refactor with new
payment flows. No real customers yet, so no cost to this ordering.
---
## What landed (2026-06-04)
**Relay (`recap-relay/`, → 0.2.119):** `identity.js` resolver +
`verifyOperatorKey`; `credits.js` `setUserTier`/`getUserCreditRow` +
`creditKey` threading; `job-credits.js`/`envelope.js` `creditKey`;
`routes/user-tier.js` (`POST`/`GET /relay/user-tier`, operator-key authed);
balance/tts/transcribe/analyze/transcribe-url/summarize-url use
`resolveIdentity`; `config.js` `relay_cloud_operator_key`; admin Settings
expose it as a masked **"Cloud operator key"** field (dashboard +
`PUT /admin/settings`).
**Recaps (`recap/`, → 0.2.143):** `db.js` `users.tier` column +
`migrateUsersTier`; `relay-state.js` `computeCreditKey` keys `user:<id>`;
`providers/index.js` `pickRelayIdentity` emits the cloud identity for paid
users; `providers/relay.js` sends `X-Recap-User-Id`+`X-Recap-Operator-Key`
and adds `setRelayUserTier`/`getRelayUserTier`; `license.js` `viewForTier`;
`license-middleware.js` `/api/license-status` derives the view from
`req.user.tier`; `tts-routes.js` gate reads `req.user.tier`; **3 gates that
keyed off `!keysat_license` now exclude paid-tier users** so they aren't
misrouted to the free-tenant `tenant_credits` path
(`/api/relay/status` display, `/api/process` gate+debit); `config.js` polls
`recap_relay_operator_key` into a live binding; `relay-default.js`
`getRelayOperatorKey` reads env→live-binding; StartOS **"Set Relay Operator
Key"** action + config field; operator **Tenants panel** gets a per-row
tier badge + **Tier** selector (`POST /api/admin/tenants/:id/tier`, which
writes the relay first then caches `users.tier`).
## Install + configure runbook
1. `cd recap-relay && make x86 && make install` (relay 0.2.119). **Never**
`make deploy`/`redeploy` for the relay.
2. Relay dashboard → Settings → Endpoints & credentials → **Cloud operator
key** → paste a fresh secret (`openssl rand -hex 32`). Save.
3. `cd recap && make x86 && make install` (app 0.2.143).
4. Recaps StartOS → Actions → **Set Relay Operator Key** → paste the **same**
secret. (Picked up within one config poll — no restart.)
5. Sign in as operator → **Tenants** → open a user's row → **Tier****Max**
(or Pro). This writes the relay `user:<id>` tier, then caches `users.tier`.
A 502 here means the two operator keys don't match — fix + retry.
6. As that user: confirm the **MAX/PRO badge**, the **Listen** (TTS) button,
that a summarize run is metered against the relay `user:<id>` pool (not
`tenant_credits`), and that `/api/relay/status` shows the relay balance.
7. Regression: a self-hosted-style license request still works (license/
install path untouched — additive).