Files
smirk-backend-core/migrations/20260702000001_premium.sql
T
jwinterm eb98e4d625 feat(premium): recurring tiered relay-posting subscription (premium-post)
A paid premium tier gating GENERAL Nostr posting to the operator's relay, while
wallet-functional events (encrypted DMs, tips, swap coordination) stay free for
registered users. Registration itself stays free (PoW-gated). Off by default.

- Admission: a new `premium-post` write policy gates on kind x membership —
  premium author -> any kind; registered + WALLET_KINDS (1059 gift-wrap, 10050
  DM-relay-list) -> free; registered + general -> premium required; external ->
  gift-wrap DM inbox only. nauthz resolves is_premium_npub (only under premium-post),
  fail-closed.
- Payment: reuses the PaymentProvider seam (PULL model, non-custodial — only reads
  invoice status). POST /premium/invoice {plan} mints a tier invoice bound to the
  JWT user_id; POST /premium/activate verifies Settled + consumes single-use AND
  extends premium_until in ONE transaction (a mid-flight failure keeps the paid
  invoice redeemable); GET /premium/status reads it. Per-user pending-invoice cap.
- Tiered plans (PREMIUM_PLANS id:days:amount) = a built-in discount feature;
  advertised via /capabilities (premium_relay + premium{currency,plans,relay_url}).
- Config fail-closes: PREMIUM_ENABLED needs relay on + premium-post + processor
  creds + >=1 positive-decimal plan + unique ids + days<=3660, and the 0-conf /
  expiry processor floors apply on the premium path too (its common case is
  require_payment=false).
- Tests: 15 policy unit (incl. 5 premium-post) + config fail-closed matrix + plan
  parser + L1 integration (activate single-use/stacking/cross-user, is_premium by
  expiry, feature gate). OpenAPI + migration updated.

Adversarially reviewed (correctness / security-payment / operational-config); the
0-conf-on-premium-path double-spend footgun, non-transactional activate, and
unbounded plan days were caught and fixed before commit.
2026-07-02 17:56:44 -04:00

27 lines
1.3 KiB
SQL

-- Premium tier: recurring, tiered access to the operator's Nostr relay for
-- general posting (the `premium-post` write policy). Wallet-functional events
-- stay free; premium unlocks general Nostr publishing.
--
-- `premium_until` is the subscription expiry (NULL = never premium). No index: the
-- hot check (is_premium_npub) is served by the existing UNIQUE index on
-- users.nostr_pubkey, and the `premium_until > NOW()` predicate is a cheap per-row
-- filter — a standalone index would only add write cost to every users UPDATE.
ALTER TABLE users ADD COLUMN premium_until TIMESTAMPTZ;
-- Mirrors payment_invoices, but binds on the authenticated user_id (a logged-in
-- premium purchase) and carries the plan's period. Single-use on redemption,
-- same atomic consume pattern. Only public data — never funds or keys.
CREATE TABLE premium_invoices (
invoice_id TEXT PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
plan_id TEXT NOT NULL,
period_days INTEGER NOT NULL,
amount TEXT NOT NULL,
currency TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
consumed_at TIMESTAMPTZ
);
CREATE INDEX premium_invoices_unconsumed_idx
ON premium_invoices (user_id) WHERE consumed_at IS NULL;