Files
recap/docs/self-serve-purchase-plan.md
T
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

5.0 KiB
Raw Blame History

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.