docs(examples): operator cookbook — six deployment recipes + premium-relay spec

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.
This commit is contained in:
jwinterm
2026-07-02 19:47:46 -04:00
parent dec9994e06
commit d96f457b69
15 changed files with 370 additions and 93 deletions
+31 -17
View File
@@ -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
+54 -46
View File
@@ -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
+20 -20
View File
@@ -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).
+54
View File
@@ -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=
+9
View File
@@ -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).
+56
View File
@@ -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 <n>`.
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=
+10
View File
@@ -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 <n>` and hand them out.
- **Relay:** you host user messages — review the content notes in
[`../LEGAL_AND_OPS.md`](../LEGAL_AND_OPS.md).
+61
View File
@@ -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=
+13
View File
@@ -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).
+7 -4
View File
@@ -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
+21
View File
@@ -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.
+3 -1
View File
@@ -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.
+11
View File
@@ -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.
+8 -5
View File
@@ -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
+12
View File
@@ -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)).