Files
recap/docs/self-serve-purchase-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

95 lines
5.0 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.
# Self-Serve Pro/Max Purchase — Implementation Plan
**Status:** Phases 14 BUILT + INSTALLED + LIVE on immense-voyage.local
(relay 0.2.121, app 0.2.153) as of 2026-06-08. Bitcoin rail end-to-end test
pending; card rail awaits operator Zaprite config. Phase 5 (SMTP expiry
reminders) remains. Follows the core decoupling (the relay owns tiers,
keyed by user-id) — this lets users buy their own Pro/Max instead of the
operator granting them by hand.
**Phase 4 (cards via Zaprite) — DONE.** Symmetric with the Bitcoin rail
(one-time prepaid checkout, NOT Zaprite recurring): relay `zaprite-client.js`
(POST /v1/orders + GET /v1/orders/:id), `POST /relay/tier-zaprite-order`
(operator-keyed), `POST /relay/zaprite/webhook` (re-fetch-to-verify — no
signature needed; both rails land at extendUserTier). Config:
`relay_zaprite_{api_key,base_url,currency}` + `relay_tier_prices_fiat_cents_json`
(default $21/$42), set via the new "Set Zaprite Connection" StartOS action.
`/api/billing/buy` gained `method:"card"`; tier-plans reports `card_available`
+ fiat prices; UI shows "Pay by card · $21" only when Zaprite is configured.
Operator TODO: run "Set Zaprite Connection" (paste API key) + register the
webhook at `https://<relay-host>/relay/zaprite/webhook` in Zaprite.
## Decisions (from Grant)
- **Prepaid periods, NOT auto-recurring.** A user pays for a fixed period
(default **30 days**) of Pro/Max. Near expiry they get an email reminder
and pay again to extend. At expiry, the tier drops to Core. No stored
payment method, no card-vault / dunning.
- **Two payment rails:**
- **Bitcoin / Lightning via BTCPay** — the *preferred* path. Relay already
has `btcpay-client.js` (createInvoice + webhook HMAC) used for credit
purchases; extend it for tier purchases.
- **Cards via Zaprite** — secondary. Grant has a Zaprite org + API +
webhooks. New integration.
- **UI:** the main pill button is **"Pay with Bitcoin"** (opens a BTCPay
invoice). Directly below it, a smaller **"Pay by card"** link (opens a
Zaprite checkout).
- **Expiry reminders via email.** Set up SMTP (Amazon SES per the Start9
recommendation) and send an automated "your Pro/Max expires in N days"
email. Recaps already has an SMTP transport (magic-link emails).
- **Prices:** from the relay's `relay_tier_prices_usd_json` (today Pro $5 /
Max $15 per period), USD-denominated, paid in sats for BTCPay.
## Phases (each shippable on its own)
### Phase 1 — Relay: prepaid tier model + expiry enforcement (foundation)
Rail-agnostic. Everything else depends on it.
- `setUserTier({userId, tier, periodDays})` → set tier, set
`subscription_expires_at`. **Extend from the current expiry** if the user
is already active (so paying early adds time, doesn't reset it).
- **Enforce expiry:** when the relay resolves a user's tier (identityTier /
the metered-route gate), treat `subscription_expires_at < now` as Core.
Add a lazy check + a periodic sweep so expired users actually drop.
- Keep the operator-grant path (`/relay/user-tier`) working — it's the comp
tool. A manual grant can set no-expiry (operator comp) vs a purchase sets
a dated period.
### Phase 2 — Bitcoin/Lightning purchase (BTCPay)
- Relay: `POST /relay/tier-invoice` (operator-key authed) — body
`{user_id, tier, period_days}``createInvoice` with metadata
`{kind:"tier_subscription", userId, tier, periodDays}` → returns the
checkout URL + invoice id.
- Relay webhook: on a settled `tier_subscription` invoice → `setUserTier`
(extend by periodDays). (Mirrors the existing credit-purchase webhook
branch.)
- Recaps: a `pending_purchases`-style record + the settle→poll→cache-tier
loop (reuse the credit-purchase machinery). On settle, refresh
`/api/license-status` so the badge flips to Pro/Max.
### Phase 3 — Purchase UI
- Tier picker (Pro / Max, price, "30 days") with the **"Pay with Bitcoin"**
pill + **"Pay by card"** link. Reuse / replace the existing buy modal.
- Bitcoin → opens the BTCPay checkout (Phase 2). Card → opens Zaprite
(Phase 4).
### Phase 4 — Cards via Zaprite
- Relay (or Recaps): create a Zaprite checkout for the tier (Grant's org +
API), metadata carrying `{userId, tier, periodDays}`.
- Zaprite webhook → verify signature → `setUserTier` (extend). Same landing
point as the BTCPay webhook.
- Wire the "Pay by card" link to it.
### Phase 5 — Expiry reminder emails
- SMTP via SES (Grant sets up SES + StartOS System SMTP; Recaps' transport
already exists).
- Periodic job: find users with `subscription_expires_at` in ~N days, email
a "renew" notice with a link back to the purchase UI. Idempotent (don't
double-send).
## Notes / open defaults (sensible unless Grant says otherwise)
- Period = 30 days. Grace = none beyond the advance email (downgrade on
expiry). Extend-from-current-expiry on early renewal.
- Relay stays **`make install` only** (private — never registry-deploy).
- The operator-key path authenticates Recaps→relay for invoice creation, the
same as the tier-grant flow.