0ae59f3550
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
95 lines
5.0 KiB
Markdown
95 lines
5.0 KiB
Markdown
# Self-Serve Pro/Max Purchase — Implementation Plan
|
||
|
||
**Status:** Phases 1–4 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.
|