From d96f457b6946f044637138535fc9be08c82ef0f9 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Thu, 2 Jul 2026 19:47:46 -0400 Subject: [PATCH] =?UTF-8?q?docs(examples):=20operator=20cookbook=20?= =?UTF-8?q?=E2=80=94=20six=20deployment=20recipes=20+=20premium-relay=20sp?= =?UTF-8?q?ec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ready-to-run .env + README recipes (personal, public-free, friends, paid-access, paid-relay, community) and the premium-relay design overview, with audit fixes to the config template and spec: - Drop the inert INSTANCE_ID knob (config never read it) and document the real per-node id, which is derived from ADMIN_PUBLIC_URL. - Warn that a placeholder PUBLIC_API_URL is rejected at boot in production (Nostr identity is on by default); soften the recipe "boots today" status. - Remove dead template vars: BTC/LTC Core-RPC settings and the non-existent PUBLIC_EXPOSE_FEATURES (replaced with the real per-field landing switches). - Mark ALTCHA_HMAC_KEY conditional in PoW-off recipes; blank placeholder payment creds so a recipe doesn't ship a key that fails validation. - Fix premium-relay spec drift: the migration creates no premium_until index, disabled endpoints return 400/200 (not 404), and neutralize operator voice. --- .env.example | 48 +++++++++----- docs/design/premium-relay.md | 100 ++++++++++++++++-------------- examples/README.md | 40 ++++++------ examples/community/.env.example | 54 ++++++++++++++++ examples/community/README.md | 9 +++ examples/friends/.env.example | 56 +++++++++++++++++ examples/friends/README.md | 10 +++ examples/paid-access/.env.example | 61 ++++++++++++++++++ examples/paid-access/README.md | 13 ++++ examples/paid-relay/.env.example | 11 ++-- examples/paid-relay/README.md | 21 +++++++ examples/personal/.env.example | 4 +- examples/personal/README.md | 11 ++++ examples/public-free/.env.example | 13 ++-- examples/public-free/README.md | 12 ++++ 15 files changed, 370 insertions(+), 93 deletions(-) create mode 100644 examples/community/.env.example create mode 100644 examples/community/README.md create mode 100644 examples/friends/.env.example create mode 100644 examples/friends/README.md create mode 100644 examples/paid-access/.env.example create mode 100644 examples/paid-access/README.md create mode 100644 examples/paid-relay/README.md create mode 100644 examples/personal/README.md create mode 100644 examples/public-free/README.md diff --git a/.env.example b/.env.example index 30bfbfd..05d7f63 100644 --- a/.env.example +++ b/.env.example @@ -6,14 +6,13 @@ # ── Server ──────────────────────────────────────────────────────────────── SERVER_HOST=0.0.0.0 SERVER_PORT=8080 -# Public absolute API base URL. REQUIRED when admin/erasure features are enabled: -# it is the canonical value the NIP-98 `u` tag is verified against (never the -# request Host header). Boot fails if unset while those features are on. +# Public absolute API base URL — set this to YOUR real domain. It is the canonical +# value the NIP-98 `u` tag is verified against (never the request Host header). In +# production, boot fails closed if this is left as a placeholder (contains `example` +# or `your-`) while Nostr identity (on by default), admin, or erasure is enabled — +# so every real deployment must replace it. PUBLIC_API_URL=https://backend.example.org/api/v1 ENVIRONMENT=development -# Stable per-node identifier. Bound into signed admin/erasure actions so a proof -# captured against one node cannot be replayed across a load-balanced fleet. -INSTANCE_ID= # ── Database ────────────────────────────────────────────────────────────── DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk @@ -31,6 +30,9 @@ REFRESH_TOKEN_PEPPER= IP_SALT= # ── Proof-of-work signup gate (ALTCHA v2) ───────────────────────────────── +# FEATURE_POW is the master switch; POW_REQUIRED only applies when it is on. Boot +# fails closed if POW_REQUIRED=true while FEATURE_POW=false (a silently-open gate). +FEATURE_POW=false ALTCHA_HMAC_KEY= POW_REQUIRED=false ALTCHA_COST=100000 @@ -64,21 +66,13 @@ PAYMENT_CURRENCY=XMR PAYMENT_CONFIRMATIONS=1 PAYMENT_EXPIRES_MINUTES=60 -# ── Bitcoin ─────────────────────────────────────────────────────────────── +# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ───── BTC_NETWORK=mainnet -BTC_USE_LOCAL_NODE=false -BTC_RPC_URL=http://127.0.0.1:8332 -BTC_RPC_USER=bitcoinrpc -BTC_RPC_PASS= BTC_ELECTRUM_URL= BTC_ELECTRUM_FALLBACKS= -# ── Litecoin ────────────────────────────────────────────────────────────── +# ── Litecoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ──── LTC_NETWORK=mainnet -LTC_USE_LOCAL_NODE=false -LTC_RPC_URL=http://127.0.0.1:9332 -LTC_RPC_USER=litecoinrpc -LTC_RPC_PASS= LTC_ELECTRUM_URL= LTC_ELECTRUM_FALLBACKS= @@ -163,11 +157,27 @@ RELAY_ADMISSION_ALLOW_PUBLIC=false RELAY_MAX_EVENT_BYTES=65536 RELAY_RETENTION_DAYS=30 +# ── Premium relay tier (optional; recurring, tiered relay-posting access) ── +# Wallet use stays free; premium unlocks GENERAL Nostr posting to the relay +# (wallet-functional events — DMs, tips, swap coordination — stay free). Requires +# RELAY_ENABLED=true + RELAY_WRITE_POLICY=premium-post + the PAYMENT_* processor +# block above. Off by default. See docs/design/premium-relay.md. +PREMIUM_ENABLED=false +# Price currency: a fiat code the processor converts, or a specific coin. +PREMIUM_CURRENCY=USD +# Comma-separated plans, each `id:days:amount` (a built-in tiered discount). +PREMIUM_PLANS=quarter:90:5,halfyear:182:9,year:365:15 + # ── Admin surface (see docs; default posture is loopback/Tor) ────────────── # Admin auth is "Sign in with Smirk": only admin PUBLIC keys are stored here, # never a seed or private key. The allowlist is mutated CLI/loopback-only in v1. ADMIN_ENABLED=false ADMIN_BIND=127.0.0.1:8081 +# Public absolute URL of THIS node's admin surface. The per-node id that binds +# signed admin actions (so a challenge signed for one node can't be replayed to +# another across a fleet) is DERIVED from this value — give each node in a fleet a +# distinct one. Must be an absolute URL when ADMIN_ENABLED=true. +ADMIN_PUBLIC_URL=https://admin.example.org ADMIN_JWT_SECRET= # Integrity MAC secret protecting admin/setup trust anchors against DB tampering. # Losing this locks out admin — back it up (see docs/operations). @@ -177,7 +187,11 @@ ADMIN_PENDING_KEY_TTL_DAYS=7 # ── Public landing page (OFF by default; full + per-field tunable when on) ── PUBLIC_LANDING_ENABLED=false -PUBLIC_EXPOSE_FEATURES=false +# Optional custom landing title (leave unset for the default). +#PUBLIC_LANDING_TITLE= +PUBLIC_EXPOSE_VERSION=false +PUBLIC_EXPOSE_CHAINS=false +PUBLIC_EXPOSE_PRICE_FEED=false PUBLIC_EXPOSE_UPTIME=false PUBLIC_STATS_ENABLED=false PUBLIC_STATS_CACHE_HOURS=24 diff --git a/docs/design/premium-relay.md b/docs/design/premium-relay.md index 80332d4..166f78b 100644 --- a/docs/design/premium-relay.md +++ b/docs/design/premium-relay.md @@ -1,18 +1,18 @@ # Spec — Premium relay access (recurring) -Status: **DRAFT / spec-first** · Owner: Such Software · Target: v0.4 +Status: **implemented** — a design overview of the as-built feature (shipped v0.3.0). ## 1. Summary A **premium tier**: registration and wallet use stay **free** (PoW-gated); an optional **recurring fee** (e.g. $5 / quarter) grants **posting access to the -Smirk-operated Nostr relay**. This reframes payment from *pay-to-register* (a +operator's Nostr relay**. This reframes payment from *pay-to-register* (a one-time gate on wallet creation) to a *subscription* (a recurring gate on relay writes). Everything else — register, unlock, balances, tip stats, receiving DMs, posting to public relays — remains free. -This is the flagship **cookbook recipe** (`examples/paid-relay/`): the live, -dogfooded demonstration of operator monetization, built entirely on seams that +This is the flagship **cookbook recipe** (`examples/paid-relay/`): a complete, +working demonstration of operator monetization, built entirely on seams that already exist (PaymentProvider + the relay admission engine). ## 2. Goals / non-goals @@ -34,18 +34,18 @@ already exist (PaymentProvider + the relay admission engine). ## 3. Model The free tier includes everything that makes the **wallet** work — including -publishing **wallet-functional events** to our relay (encrypted DMs that ride with -tips, and future P2P atomic-swap coordination). Premium adds using our relay as a -**general-purpose Nostr relay** (notes, reactions, anything). +publishing **wallet-functional events** to the operator's relay (encrypted DMs that +ride with tips, and future P2P atomic-swap coordination). Premium adds using that +relay as a **general-purpose Nostr relay** (notes, reactions, anything). | Capability | Free | Premium | |---|---|---| | Register / unlock / use wallet (all chains) | ✅ | ✅ | | Login, view tip/balance stats | ✅ | ✅ | | Receive DMs (gift-wrap delivery to you) | ✅ | ✅ | -| Publish **wallet events** to the Smirk relay (encrypted DMs, tips, swap coordination) | ✅ | ✅ | +| Publish **wallet events** to the operator's relay (encrypted DMs, tips, swap coordination) | ✅ | ✅ | | Publish to **public** relays (damus, nos.lol…) | ✅ | ✅ | -| Publish **general Nostr** to the Smirk relay (kind-1 notes, reactions, …) | ❌ | ✅ while `premium_until > now` | +| Publish **general Nostr** to the operator's relay (kind-1 notes, reactions, …) | ❌ | ✅ while `premium_until > now` | The value line: **the relay is free for what makes your wallet work; you pay to use it as your everyday Nostr relay.** Free users are never cut off from wallet features. @@ -55,7 +55,9 @@ it as your everyday Nostr relay.** Free users are never cut off from wallet feat Migration `..._premium.sql`: ```sql ALTER TABLE users ADD COLUMN premium_until TIMESTAMPTZ; -- NULL = never premium -CREATE INDEX users_premium_until_idx ON users (premium_until); +-- No standalone index on premium_until: is_premium_npub keys on the existing UNIQUE +-- index on users.nostr_pubkey and `premium_until > NOW()` is a cheap per-row filter, +-- so an index here would only add write cost to every users UPDATE. ``` A **dedicated `premium_invoices` table** (not a `purpose` column on `payment_invoices`), because premium binds on the authenticated `user_id` (a logged-in purchase) while @@ -101,14 +103,16 @@ loopback-admission requirement (non-open policies need the admission service). ## 6. Payment flow (recurring + tiered, reuses the PaymentProvider seam) - `POST /premium/invoice` (JWT auth), body `{ plan }` → mint an invoice for the - selected plan's amount via the PaymentProvider, atomically bound to the caller with - `purpose='premium'` and the plan's `days`. Returns the invoice (pay URL / address). -- Settle (pull model, same poller as pay-to-register): on a `premium` invoice - settling, call `extend_premium(user_id, plan_days)` **once** (idempotent on invoice - id). Never holds funds — pull-only. + selected plan's amount via the PaymentProvider, recorded in `premium_invoices` + bound to the caller's `user_id` with the plan's `days`. Returns the invoice (pay + URL / address). +- `POST /premium/activate` (JWT auth), body `{ invoice_id }` → the client-driven + settle path (pull model, no background poller): verify the invoice is `Settled` at + the processor, then `activate_premium` (single-use consume + `premium_until` extend, + in one transaction). Never holds funds — status reads only. - `GET /premium/status` (JWT auth) → `{ active, premium_until }`. -**Tiered plans (a discount feature we build + exercise).** `PREMIUM_PLANS` is a list +**Tiered plans (a built-in discount feature).** `PREMIUM_PLANS` is a list of `{ id, days, amount }`: | id | days | price | vs. quarterly | @@ -127,12 +131,11 @@ Config: `PREMIUM_ENABLED` (default false), `PREMIUM_CURRENCY`, `PREMIUM_PLANS` `PREMIUM_ENABLED` ⇒ provider configured **and** `RELAY_ENABLED` **and** `RELAY_WRITE_POLICY=premium-post` **and** ≥1 valid plan (fail closed otherwise). -## 6.5 Resource governance — the box is shared +## 6.5 Resource governance — a shared host -The reference instance runs the relay **on the same box as the wallet backend and the -LWS scanners**, so the relay must stay within a budget and never starve them. A paid -base is naturally bounded (only paying users post general content), but we set hard -limits regardless: +When the relay runs **on the same host as the wallet backend and the LWS scanners**, +it must stay within a budget and never starve them. A paid base is naturally bounded +(only paying users post general content); set hard limits regardless: - **Size + retention** — `RELAY_MAX_EVENT_BYTES` (64 KB) and `RELAY_RETENTION_DAYS` (30) cap storage; general (non-DM) content can carry a shorter retention than gift-wraps if needed. @@ -141,9 +144,9 @@ limits regardless: monopolize CPU / DB / bandwidth. - **DB isolation** — the relay uses its own tables + retention job; the wallet backend's hot auth / LWS paths are untouched. -- **Watch + escape hatch** — the `paid-relay` recipe ships a suggested cap set and - says: if relay load approaches the wallet backend's headroom, move the relay to its - own box — the seam already supports `RELAY_MODE=external`. +- **Escape hatch** — the `paid-relay` recipe ships a suggested cap set: if relay load + approaches the wallet backend's headroom, move the relay to its own host — the seam + already supports `RELAY_MODE=external`. ## 7. Capabilities + OpenAPI @@ -152,14 +155,14 @@ limits regardless: so the client renders the plan tiers with the discount visible. The client greys the **general-Nostr-posting** affordance unless `premium.active`; wallet events + DMs stay available free. -- OpenAPI: document `POST /premium/invoice` + `GET /premium/status`; the existing - **OpenAPI no-drift check** must pass. Capabilities **golden test** updated to - include `premium_relay` + `premium`. +- OpenAPI: document `POST /premium/invoice`, `POST /premium/activate`, and + `GET /premium/status`; the existing **OpenAPI no-drift check** must pass. + Capabilities **golden test** updated to include `premium_relay` + `premium`. ## 8. Free ↔ premium enforcement points (defense in depth) 1. **Relay admission (authoritative):** the gRPC hook denies non-premium *general* - writes to our relay regardless of client behavior (wallet events stay free). This + writes to the relay regardless of client behavior (wallet events stay free). This is the real gate. 2. **Client UX:** reads `premium.active` from `/premium/status` and disables the affordance — a courtesy, never the enforcement. @@ -169,31 +172,36 @@ The client is never trusted; a user who bypasses the UI still hits admission-den ## 9. Migration / compatibility - Additive column; existing users get `premium_until = NULL` (free). No break. -- v0.2 backend unaffected. `PREMIUM_ENABLED=false` (default) → zero behavior change; - `/premium/*` return 404/feature-off and `/capabilities` omits `premium`. +- v0.2 backend unaffected. `PREMIUM_ENABLED=false` (default) → zero behavior change: + `POST /premium/invoice` and `/premium/activate` return **400** (`VALIDATION_ERROR`, + "Premium is not available on this instance."), `GET /premium/status` returns **200** + with `{ active: false, premium_until: null }`, and `/capabilities` omits the + `premium` object (`features.premium_relay: false`). - The relay stays **off** until an operator opts in (`RELAY_ENABLED=true` + - `premium-post`); `api.smirk.cash` enables it as the reference paid relay. + `premium-post`) — see the `examples/paid-relay/` recipe. ## 10. Testing (Ps and Qs) -- **Unit** — `policy.rs`: PremiumPost permit (premium author), deny (lapsed / - non-premium), gift-wrap delivery to premium recipient, PoW on external inbound. -- **Unit** — `extend_premium` stacks from `max(now, current)`; `is_premium_npub` - respects expiry (peppered lookup). -- **L2** — admission service: premium author PERMIT, lapsed author DENY. -- **L2** — settle handler: a `premium` invoice extends `premium_until` once (idempotent). +- **Unit** — `policy.rs`: PremiumPost permits a premium author (any kind), permits a + registered author's wallet kinds, denies a registered author's general post, and + delivers an external gift-wrap to a registered recipient (else denies). +- **Unit** — config fail-closed matrix (relay / policy / plans / creds / 0-conf / + days cap) and the `PREMIUM_PLANS` parser. +- **Integration** — `activate_premium` is single-use, stacks from `max(now, current)`, + and rejects a cross-user invoice; `is_premium_npub` respects expiry (npub is public, + not peppered); the premium routes are feature-gated off by default. - **Golden** — capabilities includes `premium_relay` + `premium` when enabled, omits when off. -- **No-drift** — OpenAPI covers the two new endpoints. -- **E2E** — pay invoice → post to Smirk relay PERMITted → fast-forward past expiry - → post DENIED → renew → PERMITted again. +- **No-drift** — OpenAPI covers the three premium endpoints. -## 11. Open questions +## 11. Design decisions & limitations -1. **Grace period** on expiry (e.g. 3-day soft window) vs hard cutoff? (Lean hard; - `extend_premium` early-renewal stacking softens it.) -2. **Read-gating** (NIP-42) — defer to v2; note it in the cookbook recipe. -3. **Default price/period** for the reference instance — product call. -4. **Refunds** — none (prepaid access to a service); state in ToS/recipe README. +1. **Expiry is a hard cutoff** — no grace window; early-renewal stacking + (`activate_premium` extends from `max(now, current)`) softens it. +2. **Writes are gated, reads stay open** — read-gating would need NIP-42 AUTH plus a + premium check; a possible future addition. +3. **Prepaid periods, not auto-charge** — crypto has no card-on-file, so a member + re-pays each term or posting lapses (honest + simple). +4. **No refunds** — prepaid access to a service; stated in the recipe README / ToS. ## 12. Cookbook tie-in diff --git a/examples/README.md b/examples/README.md index f6064a2..d17cb2b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,16 +10,17 @@ Each recipe is a directory with: called out; copy the full `../../.env.example` and apply the deltas). - **`README.md`** — what it's for, what it costs to run, the legal/operational notes. -> **Status.** `personal/`, `public-free/`, and `paid-relay/` (the flagship — it -> uses the shipped [premium-relay feature](../docs/design/premium-relay.md)) have -> full `.env.example`s. `friends/`, `paid-access/`, and `community/` are summarized -> below and get their own directories on the next pass. +> Each recipe below has its own directory with a tuned `.env.example` and a short +> README. `paid-relay/` is the flagship — it uses the shipped +> [premium-relay feature](../docs/design/premium-relay.md). ## Every recipe, the same three steps 1. `cp ../../.env.example .env`, then apply the recipe's deltas. -2. Generate each secret: `openssl rand -hex 32` (`JWT_SECRET`, `SEED_FINGERPRINT_PEPPER`, - `REFRESH_TOKEN_PEPPER`, `IP_SALT`, `ALTCHA_HMAC_KEY`, `ADMIN_*_SECRET`). Fill chain - endpoints. `chmod 600 .env`. +2. Set `PUBLIC_API_URL` to your real domain (a placeholder is rejected at boot in + production). Generate each secret: `openssl rand -hex 32` (`JWT_SECRET`, + `SEED_FINGERPRINT_PEPPER`, `REFRESH_TOKEN_PEPPER`, `IP_SALT`, `ADMIN_*_SECRET`, plus + `ALTCHA_HMAC_KEY` only if the recipe sets `FEATURE_POW=true`). Fill chain endpoints. + `chmod 600 .env`. 3. Run the binary (migrations apply on boot). Reverse-proxy TLS; bind the API to `127.0.0.1`. @@ -30,12 +31,12 @@ funds. **Self-hosting bypasses every gate.** Secrets are never committed. | Recipe | For | Registration | Relay | Pitch | Status | |---|---|---|---|---|---| -| [`personal/`](personal/) | Just you | open, no gate | off (or open, just you) | Your own sovereign backend | ✅ boots today | -| [`public-free/`](public-free/) | A free public instance | **open + PoW** | off (v1) | What `api.smirk.cash` runs | ✅ boots today | -| `friends/` | Your circle | **invite-only** | on, `inbox-outbox` | A backend for people you trust | 📝 2nd pass | -| `paid-access/` | A paid wallet service | **pay-to-register** | optional | Charge once for a hosted wallet backend | 📝 2nd pass | -| `paid-relay/` | Monetize a relay | **open + PoW** (free wallet) | on, **`premium-post`** | Free wallet, paid ($/quarter) Nostr posting | ✅ premium-relay shipped | -| `community/` | A project/DAO space | invite | on, `author-allowlist` | A gated relay for a named group | 📝 2nd pass | +| [`personal/`](personal/) | Just you | open, no gate | off (or open, just you) | Your own sovereign backend | ✅ set domain + secrets | +| [`public-free/`](public-free/) | A free public instance | **open + PoW** | off (v1) | The shape a free public instance runs | ✅ set domain + secrets | +| [`friends/`](friends/) | Your circle | **invite-only** | on, `inbox-outbox` | A backend for people you trust | ✅ | +| [`paid-access/`](paid-access/) | A paid wallet service | **pay-to-register** | optional | Charge once for a hosted wallet backend | ✅ | +| [`paid-relay/`](paid-relay/) | Monetize a relay | **open + PoW** (free wallet) | on, **`premium-post`** | Free wallet, paid ($/quarter) Nostr posting | ✅ premium-relay shipped | +| [`community/`](community/) | A project/community space | invite | on, `author-allowlist` | A gated relay for a named group | ✅ | ## Recipe summaries (config deltas) @@ -52,8 +53,8 @@ funds. **Self-hosting bypasses every gate.** Secrets are never committed. processor paying *your* wallet). A one-time access fee gates wallet creation. Non-custodial; you never touch funds (pull-model invoice reads only). - **`paid-relay`** — free wallet (open + PoW) **plus** `PREMIUM_ENABLED=true`, - `RELAY_ENABLED=true`, `RELAY_WRITE_POLICY=premium-post`, `PREMIUM_AMOUNT` / - `PREMIUM_CURRENCY` / `PREMIUM_PERIOD_DAYS`. Recurring revenue for relay posting; reads + `RELAY_ENABLED=true`, `RELAY_WRITE_POLICY=premium-post`, `PREMIUM_CURRENCY` + + `PREMIUM_PLANS` (`id:days:amount`). Recurring revenue for relay posting; reads open (v1). **Content posture:** running a relay carries user content — see the NCMEC/DMCA clauses in the published Terms (§6, §9.1). Spec: [premium-relay](../docs/design/premium-relay.md). - **`community`** — `REGISTRATION_REQUIRE_INVITE=true`, `RELAY_ENABLED=true` + @@ -61,8 +62,7 @@ funds. **Self-hosting bypasses every gate.** Secrets are never committed. ## Choosing -- Want **freedom**? `personal` (or `friends` for a group). -- Want a **free public good**? `public-free`. -- Want it to **pay for itself**? `paid-relay` (recurring, real utility) or `paid-access` - (one-time). `paid-relay` is the flagship — recurring revenue tied to a service people - actually value, and the live demo runs at `api.smirk.cash`. +- Solo, or a trusted group → `personal` / `friends`. +- A free public instance → `public-free`. +- Cost recovery → `paid-relay` (recurring, tied to a real service — the flagship) or + `paid-access` (a one-time signup fee). diff --git a/examples/community/.env.example b/examples/community/.env.example new file mode 100644 index 0000000..7e2e5e2 --- /dev/null +++ b/examples/community/.env.example @@ -0,0 +1,54 @@ +# ── Recipe: community ──────────────────────────────────────────────────────── +# A gated space for a named project / community: invite-only registration plus a +# relay that ONLY your registered members may publish to (author-allowlist). A +# members-only Nostr space with its own DM inbox. +# +# ⚠️ Running a relay hosts user content — see ../LEGAL_AND_OPS.md. +# +# Start from ../../.env.example and apply the lines below. openssl rand -hex 32 +# for each secret. +# ───────────────────────────────────────────────────────────────────────────── + +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +# REQUIRED: replace with YOUR real https domain. The your-domain.example placeholder +# is rejected at boot in production (Nostr identity is on by default). +PUBLIC_API_URL=https://api.your-domain.example/api/v1 +ENVIRONMENT=production + +DATABASE_URL=postgresql:///smirk?host=/var/run/postgresql + +JWT_SECRET= +SEED_FINGERPRINT_PEPPER= +REFRESH_TOKEN_PEPPER= +IP_SALT= +# ALTCHA_HMAC_KEY — only needed if you set FEATURE_POW=true (this recipe leaves PoW off). +ALTCHA_HMAC_KEY= +ADMIN_KEY_INTEGRITY_SECRET= + +# Invite-only membership (mint codes with `smirk-admin mint-invite`). +POW_REQUIRED=false +REGISTRATION_REQUIRE_INVITE=true +REGISTRATION_REQUIRE_PAYMENT=false + +WALLET_RESTORE_POLICY=unlimited +FEATURE_PRICES=true +FEATURE_TIPS=false + +# Relay on, members-only: only registered members may publish anything. +RELAY_ENABLED=true +RELAY_MODE=bundled +RELAY_URL=wss://relay.your-domain.example +RELAY_WRITE_POLICY=author-allowlist +RELAY_INBOUND_POW_BITS=0 +RELAY_ADMISSION_BIND=127.0.0.1:8090 + +# Chain endpoints — see ../../.env.example. +XMR_LWS_URL= +XMR_LWS_ADMIN_URL= +XMR_LWS_ADMIN_KEY= +XMR_DAEMON_URL= +WOW_LWS_URL= +WOW_LWS_ADMIN_URL= +WOW_LWS_ADMIN_KEY= +WOW_DAEMON_URL= diff --git a/examples/community/README.md b/examples/community/README.md new file mode 100644 index 0000000..f9e2a93 --- /dev/null +++ b/examples/community/README.md @@ -0,0 +1,9 @@ +# community — a members-only space for a project or group + +Invite-only registration plus a relay that ONLY your registered members may publish +to (`author-allowlist`) — a members-only Nostr space with its own DM inbox. + +- **Config:** [`.env.example`](.env.example) — `REGISTRATION_REQUIRE_INVITE=true`, + `RELAY_ENABLED=true`, `RELAY_WRITE_POLICY=author-allowlist`. +- **Membership:** mint codes with `smirk-admin mint-invite`. +- **Relay:** you host member content — see [`../LEGAL_AND_OPS.md`](../LEGAL_AND_OPS.md). diff --git a/examples/friends/.env.example b/examples/friends/.env.example new file mode 100644 index 0000000..c54bca9 --- /dev/null +++ b/examples/friends/.env.example @@ -0,0 +1,56 @@ +# ── Recipe: friends ────────────────────────────────────────────────────────── +# A backend for a known group. Registration is invite-only (mint a code for each +# person you trust); the relay is on with the locked-down inbox-outbox policy so +# the group has a private DM inbox. No payment, generous restore. +# +# Start from ../../.env.example and apply the lines below. openssl rand -hex 32 +# for each secret. +# ───────────────────────────────────────────────────────────────────────────── + +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +# REQUIRED: replace with YOUR real https domain. The your-domain.example placeholder +# is rejected at boot in production (Nostr identity is on by default). +PUBLIC_API_URL=https://api.your-domain.example/api/v1 +ENVIRONMENT=production + +DATABASE_URL=postgresql:///smirk?host=/var/run/postgresql + +JWT_SECRET= +SEED_FINGERPRINT_PEPPER= +REFRESH_TOKEN_PEPPER= +IP_SALT= +# ALTCHA_HMAC_KEY — only needed if you set FEATURE_POW=true (this recipe leaves PoW off). +ALTCHA_HMAC_KEY= +ADMIN_KEY_INTEGRITY_SECRET= + +# Invite-only — hand out codes with `smirk-admin mint-invite --count `. +POW_REQUIRED=false +REGISTRATION_REQUIRE_INVITE=true +REGISTRATION_REQUIRE_PAYMENT=false + +# A known group can restore as far back as they like. +WALLET_RESTORE_POLICY=unlimited + +FEATURE_PRICES=true +FEATURE_TIPS=false + +# Relay on, locked down: registered members publish their own events; anyone may +# deliver a gift-wrapped DM to a member. (Running a relay hosts user content — +# see ../LEGAL_AND_OPS.md.) +RELAY_ENABLED=true +RELAY_MODE=bundled +RELAY_URL=wss://relay.your-domain.example +RELAY_WRITE_POLICY=inbox-outbox +RELAY_INBOUND_POW_BITS=0 +RELAY_ADMISSION_BIND=127.0.0.1:8090 + +# Chain endpoints — point at your own nodes / LWS. See ../../.env.example. +XMR_LWS_URL= +XMR_LWS_ADMIN_URL= +XMR_LWS_ADMIN_KEY= +XMR_DAEMON_URL= +WOW_LWS_URL= +WOW_LWS_ADMIN_URL= +WOW_LWS_ADMIN_KEY= +WOW_DAEMON_URL= diff --git a/examples/friends/README.md b/examples/friends/README.md new file mode 100644 index 0000000..9876db1 --- /dev/null +++ b/examples/friends/README.md @@ -0,0 +1,10 @@ +# friends — a backend for a known group + +Invite-only registration plus a locked-down relay (`inbox-outbox`) so your circle +has a private DM inbox. No payment; generous restore. + +- **Config:** [`.env.example`](.env.example) — `REGISTRATION_REQUIRE_INVITE=true`, + `RELAY_ENABLED=true`, `RELAY_WRITE_POLICY=inbox-outbox`. +- **Invites:** mint codes with `smirk-admin mint-invite --count ` and hand them out. +- **Relay:** you host user messages — review the content notes in + [`../LEGAL_AND_OPS.md`](../LEGAL_AND_OPS.md). diff --git a/examples/paid-access/.env.example b/examples/paid-access/.env.example new file mode 100644 index 0000000..3206d08 --- /dev/null +++ b/examples/paid-access/.env.example @@ -0,0 +1,61 @@ +# ── Recipe: paid-access ────────────────────────────────────────────────────── +# A paid hosted wallet backend: a one-time fee gates NEW wallet creation (existing +# wallets and self-hosters bypass it). Non-custodial and pull-model — the backend +# only READS invoice status; funds go straight to your own wallet. +# +# ⚠️ Charging a fee has operational + legal implications — see ../LEGAL_AND_OPS.md. +# +# Start from ../../.env.example and apply the lines below. openssl rand -hex 32 +# for each secret. +# ───────────────────────────────────────────────────────────────────────────── + +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +# REQUIRED: replace with YOUR real https domain. The your-domain.example placeholder +# is rejected at boot in production (Nostr identity is on by default). +PUBLIC_API_URL=https://api.your-domain.example/api/v1 +ENVIRONMENT=production + +DATABASE_URL=postgresql:///smirk?host=/var/run/postgresql + +JWT_SECRET= +SEED_FINGERPRINT_PEPPER= +REFRESH_TOKEN_PEPPER= +IP_SALT= +ALTCHA_HMAC_KEY= +ADMIN_KEY_INTEGRITY_SECRET= + +# Pay-to-register: a settled invoice is required to create a new wallet. Compose +# with PoW if you like (FEATURE_POW is the master switch; POW_REQUIRED alone is +# inert). Returning wallets never pay again. +FEATURE_POW=true +POW_REQUIRED=true +REGISTRATION_REQUIRE_INVITE=false +REGISTRATION_REQUIRE_PAYMENT=true + +# Processor (BTCPay-compatible; also served by xmrcheckout / wowcheckout). Price +# is fiat-denominated here (payable in any coin the processor offers). +PAYMENT_PROVIDER=btcpay +PAYMENT_PROVIDER_URL=https://pay.your-domain.example +PAYMENT_STORE_ID= +PAYMENT_API_KEY= +PAYMENT_AMOUNT=5 +PAYMENT_CURRENCY=USD +PAYMENT_CONFIRMATIONS=1 +PAYMENT_EXPIRES_MINUTES=60 + +WALLET_RESTORE_POLICY=bounded +WALLET_MAX_RESTORE_DEPTH_DAYS=365 +FEATURE_PRICES=true +FEATURE_TIPS=false +RELAY_ENABLED=false + +# Chain endpoints — see ../../.env.example. +XMR_LWS_URL= +XMR_LWS_ADMIN_URL= +XMR_LWS_ADMIN_KEY= +XMR_DAEMON_URL= +WOW_LWS_URL= +WOW_LWS_ADMIN_URL= +WOW_LWS_ADMIN_KEY= +WOW_DAEMON_URL= diff --git a/examples/paid-access/README.md b/examples/paid-access/README.md new file mode 100644 index 0000000..903ae5e --- /dev/null +++ b/examples/paid-access/README.md @@ -0,0 +1,13 @@ +# paid-access — a paid hosted wallet backend + +A one-time fee gates creating a NEW wallet (existing wallets and self-hosters bypass +it). Non-custodial and pull-model: the backend only reads invoice status, and funds +go straight to your own wallet. + +- **Config:** [`.env.example`](.env.example) — `REGISTRATION_REQUIRE_PAYMENT=true` + + the `PAYMENT_*` processor block (BTCPay-compatible; also served by xmrcheckout / + wowcheckout). Price is fiat-denominated, payable in any coin the processor offers. +- **You are a merchant, not a money transmitter** — you sell access to your service + and never hold user funds. Have clear Terms (what the fee buys, refunds, fair use), + treat the fees as income, and see [`../LEGAL_AND_OPS.md`](../LEGAL_AND_OPS.md). +- `PAYMENT_CONFIRMATIONS >= 1` (0-conf lets a paid invoice be reversed after signup). diff --git a/examples/paid-relay/.env.example b/examples/paid-relay/.env.example index 77ac9cc..11443bd 100644 --- a/examples/paid-relay/.env.example +++ b/examples/paid-relay/.env.example @@ -13,11 +13,12 @@ SERVER_HOST=127.0.0.1 SERVER_PORT=8081 +# REQUIRED: replace with YOUR real https domain. The your-domain.example placeholder +# is rejected at boot in production (Nostr identity is on by default). PUBLIC_API_URL=https://api.your-domain.example/api/v1 ENVIRONMENT=production -INSTANCE_ID=paid-relay-1 -DATABASE_URL=postgresql:///smirk_v3_db?host=/var/run/postgresql +DATABASE_URL=postgresql:///smirk?host=/var/run/postgresql # Secrets (REQUIRED; openssl rand -hex 32 each). JWT_SECRET= @@ -28,6 +29,8 @@ ALTCHA_HMAC_KEY= ADMIN_KEY_INTEGRITY_SECRET= # ── Free registration, PoW-gated (wallet use is always free) ── +# FEATURE_POW is the master switch; POW_REQUIRED alone is inert without it. +FEATURE_POW=true POW_REQUIRED=true REGISTRATION_REQUIRE_INVITE=false REGISTRATION_REQUIRE_PAYMENT=false @@ -58,8 +61,8 @@ PREMIUM_PLANS=quarter:90:5,halfyear:182:9,year:365:15 # directly to YOUR wallet. PAYMENT_PROVIDER=btcpay PAYMENT_PROVIDER_URL=https://pay.your-domain.example -PAYMENT_STORE_ID=your-store-id -PAYMENT_API_KEY=your-processor-api-key +PAYMENT_STORE_ID= +PAYMENT_API_KEY= PAYMENT_CONFIRMATIONS=1 PAYMENT_EXPIRES_MINUTES=60 diff --git a/examples/paid-relay/README.md b/examples/paid-relay/README.md new file mode 100644 index 0000000..3ade35c --- /dev/null +++ b/examples/paid-relay/README.md @@ -0,0 +1,21 @@ +# paid-relay — free wallet, paid Nostr posting (the flagship) + +A free wallet (open + PoW) **plus** a paid premium tier for **general** Nostr +posting to your relay. Wallet-functional events — encrypted DMs, tips, swap +coordination — stay **free** for registered users; premium unlocks general posting. +Recurring revenue tied to a real service. Implements the +[premium-relay feature](../../docs/design/premium-relay.md). + +- **Config:** [`.env.example`](.env.example) — `RELAY_WRITE_POLICY=premium-post`, + `PREMIUM_ENABLED=true`, tiered `PREMIUM_PLANS` (a built-in discount curve), and the + `PAYMENT_*` processor block (reused from pay-to-register; BTCPay-compatible). +- **Free vs paid:** the relay is free for what makes the wallet work; you pay to use + it as your everyday relay. Reads stay open (v1); premium gates *writes*. +- **Money:** you sell access to your relay (a fee), non-custodial + pull-model — + never hold funds. `PAYMENT_CONFIRMATIONS >= 1`; validation rejects the unsafe combos. +- **Content:** running a relay means hosting user content — a DMCA designated agent, + a CSAM-reporting process, and acceptable-use terms apply. See + [`../LEGAL_AND_OPS.md`](../LEGAL_AND_OPS.md). +- **Resources:** the relay runs beside the wallet backend + LWS, so keep the size / + retention / connection / rate caps (in `.env.example` and the nostr-rs-relay config + under `deploy/relay`); move to `RELAY_MODE=external` on its own host if load climbs. diff --git a/examples/personal/.env.example b/examples/personal/.env.example index fd1e5d5..b2752a3 100644 --- a/examples/personal/.env.example +++ b/examples/personal/.env.example @@ -7,9 +7,10 @@ SERVER_HOST=127.0.0.1 SERVER_PORT=8080 +# REQUIRED: replace with YOUR real https domain. The your-domain.example placeholder +# is rejected at boot in production (Nostr identity is on by default). PUBLIC_API_URL=https://your-domain.example/api/v1 ENVIRONMENT=production -INSTANCE_ID=my-personal-node # Unix socket + peer auth (run the service as the DB-owning OS user) — no password. DATABASE_URL=postgresql:///smirk?host=/var/run/postgresql @@ -19,6 +20,7 @@ JWT_SECRET= SEED_FINGERPRINT_PEPPER= REFRESH_TOKEN_PEPPER= IP_SALT= +# ALTCHA_HMAC_KEY — only needed if you set FEATURE_POW=true (this recipe leaves PoW off). ALTCHA_HMAC_KEY= # No gates — it's just you. diff --git a/examples/personal/README.md b/examples/personal/README.md new file mode 100644 index 0000000..57b94c2 --- /dev/null +++ b/examples/personal/README.md @@ -0,0 +1,11 @@ +# personal — your own sovereign backend + +Run the backend for yourself alone: no registration gates, unlimited restore, the +relay optional. The smallest footprint; you are the only user. + +- **Config:** [`.env.example`](.env.example) — all gates off, + `WALLET_RESTORE_POLICY=unlimited`, relay off. +- **Boot:** copy `.env.example` → `.env`, generate the secrets (`openssl rand -hex 32`), + fill your chain endpoints, run the binary (migrations apply on boot). Bind the API + to `127.0.0.1` behind your own TLS. +- Non-custodial: the backend never sees your seed or spend keys — funds stay on-chain. diff --git a/examples/public-free/.env.example b/examples/public-free/.env.example index aa904de..9ae4c19 100644 --- a/examples/public-free/.env.example +++ b/examples/public-free/.env.example @@ -1,7 +1,7 @@ # ── Recipe: public-free ────────────────────────────────────────────────────── # A free, open public instance — anyone can register and use the wallet; spam is -# gated by proof-of-work, not payment. This is (minus secrets) what the reference -# instance api.smirk.cash runs. +# gated by proof-of-work, not payment. This is (minus secrets) the shape a free, +# open public instance runs. # # Start from ../../.env.example and apply the lines below. openssl rand -hex 32 # for each secret. @@ -9,11 +9,12 @@ SERVER_HOST=127.0.0.1 SERVER_PORT=8081 +# REQUIRED: replace with YOUR real https domain. The your-domain.example placeholder +# is rejected at boot in production (Nostr identity is on by default). PUBLIC_API_URL=https://api.your-domain.example/api/v1 ENVIRONMENT=production -INSTANCE_ID=public-1 -DATABASE_URL=postgresql:///smirk_v3_db?host=/var/run/postgresql +DATABASE_URL=postgresql:///smirk?host=/var/run/postgresql # Secrets (REQUIRED; openssl rand -hex 32 each). JWT_SECRET= @@ -23,7 +24,9 @@ IP_SALT= ALTCHA_HMAC_KEY= ADMIN_KEY_INTEGRITY_SECRET= -# Free registration, spam-gated by compute (ALTCHA proof-of-work). +# Free registration, spam-gated by compute (ALTCHA proof-of-work). FEATURE_POW is +# the master switch; POW_REQUIRED alone is inert without it. +FEATURE_POW=true POW_REQUIRED=true REGISTRATION_REQUIRE_INVITE=false REGISTRATION_REQUIRE_PAYMENT=false diff --git a/examples/public-free/README.md b/examples/public-free/README.md new file mode 100644 index 0000000..5dc785e --- /dev/null +++ b/examples/public-free/README.md @@ -0,0 +1,12 @@ +# public-free — a free, open public instance + +Anyone can register and use the wallet; spam is gated by proof-of-work, not +payment. This is the shape a free, open public instance runs. + +- **Config:** [`.env.example`](.env.example) — `POW_REQUIRED=true`, no invite/payment + gate, `WALLET_RESTORE_POLICY=bounded` + PoW-priced, relay off at launch. +- **Boot:** copy → `.env`, generate secrets, reuse your existing daemons / LWS, run + the binary. Front it with TLS; bind the API to `127.0.0.1`. +- No money changes hands, so there is no payment/consumer-sale surface — just have a + Terms + Privacy that disclose what the backend receives (see + [`../LEGAL_AND_OPS.md`](../LEGAL_AND_OPS.md)).