mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
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.
27 lines
1.3 KiB
SQL
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;
|