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.
examples/: an operator "cookbook" turning the backend's composable knobs
(registration gates, relay write policy, restore policy, feature flags) into
ready-to-run recipes per persona — personal, public-free (what the reference
instance runs), friends, paid-access, community, and paid-relay (the flagship:
free wallet + paid premium Nostr posting). Plus LEGAL_AND_OPS.md, a
not-legal-advice considerations checklist that points operators at their own
counsel and at the project's own published docs as a worked example. Recipes use
placeholders only.
docs/design/premium-relay.md: the premium-relay feature spec (reconciled to the
shipped design — dedicated premium_invoices table, transactional activate).
`smirk-admin migrate-legacy --source-url <pg> [--commit] [--limit N]` copies
user identity from a legacy v0.2.x backend database into this v0.3 DB, so users
keep their handle (and NIP-05 name@domain) when they move to a v0.3 client.
- Identity-only, joined on the derivation-independent seed_fingerprint; also
carries pubkey_hash (v3 wallets reclaim their row with no rotation step),
username, nostr_pubkey, wallet_birthday and chain restore heights. Socials/
tips/keys/wallets are intentionally not carried (no v0.3 home, or re-registered
by the client on first unlock).
- pubkey_hash + seed_fingerprint are peppered at rest, so the importer builds its
target with the real SEED_FINGERPRINT_PEPPER (run with the v0.3 .env).
- Idempotent (skips users already present by either peppered identity column),
dry-run unless --commit, best-effort per row: a transient error or a concurrent
native onboarding is reconciled, never aborting the pass. Reserved/taken/
malformed usernames and already-linked npubs are dropped; the user still lands.
- Exports validate_username so the importer applies the identical format +
reserved-name rules the HTTP handler does.
- docs/operations/LEGACY_MIGRATION.md + scripts/test-legacy-migration.sh, a
self-contained E2E that seeds alice/bob/carol plus edge cases and asserts
import, peppering, collision handling, and idempotency.
Reviewed adversarially (correctness / security / operational) before commit.
Adversarial review of the relay admission surface:
- FAIL-SAFE on admission death: nostr-rs-relay fails OPEN if its gRPC admission
server is unreachable, so a dead admission = an unrestricted relay. The backend
now EXITS if the admission service stops (supervisor restarts a clean pair);
documented in RELAY.md. Pin the relay image to a digest (compose note).
- Registration-oracle protection: validate() now refuses a non-loopback
RELAY_ADMISSION_BIND unless RELAY_ADMISSION_ALLOW_PUBLIC=true (+ test).
- No N+1 on crafted events: recipient p-tags are capped (20, deduped) and resolved
in ONE batched query (db.any_registered_npub) instead of one-per-tag.
- Defence-in-depth size cap: admission denies events over max_event_bytes before
any DB work (the relay enforces its own limit too).
- Docs: RELAY.md Security section (fail-open, loopback oracle, default PoW=0 spam
footgun, client sender-verification); config.toml relay port 8088 (not 8080);
Docker Hub image; .env.example RELAY_ADMISSION_ALLOW_PUBLIC.
Note: the client-side sender-impersonation blocker is fixed separately in the
monorepo (verifying gift-wrap unwrap).
Completes the operator side of the relay integration:
- deploy/relay/: reference docker-compose + config.toml for a locked-down
nostr-rs-relay wired to the backend's gRPC admission (host networking so the
loopback admission socket stays private). Clearly marked a template to pin/
verify against the operator's nostr-rs-relay version.
- docs/operations/RELAY.md: what it is (inbox relay, not the only relay), the
admission architecture, the three write policies + NIP-13 PoW, bundled vs
external, verification, and the seam's modularity. Linked from OPERATOR_SETUP.
- .env.example: the RELAY_* block (all off/placeholder).
Backend relay integration (config → seam → policy → gRPC admission →
capabilities → NIP-05 → packaging/docs) is now complete. Client DM stack next.
create-admin-wallet registered a pending admin key but never latched setup, so on
a fresh instance it left the bootstrap half-open and a later `setup` collided with
it (setup's guard only checked for an ACTIVE admin; the cli key was pending).
- New atomic Database::create_admin_key_bootstrapping: under the admin-keys
advisory lock, insert the pending generated key AND latch `locked` (unless
already validly locked), append audit, commit — one transaction. Fails CLOSED on
a tampered latch (defers to reset-setup) instead of silently healing it.
- bootstrap_admin (setup) now refuses on ANY live admin key (active OR pending),
not just active, so setup composes with create-admin-wallet. The adopt-existing-
deployment path (users but no admin keys) is unaffected: no live key -> proceeds.
- reset_setup now takes the same advisory lock (all latch writers serialize) and
documents the live-key recovery path.
- CLI prints bootstrapped-vs-added and a terminating lost-secret recovery hint.
- Docs (README + OPERATOR_SETUP section 5): both mutually-exclusive bootstrap paths.
Adversarially reviewed (atomicity / lockout / regression — all ship). L1 test
extended: fresh-latch, no-relatch-when-locked, pending semantics, composition
guard, and tamper-refuse (fails closed + rolls back the key insert).
- .env.example: add the required at-rest identity secrets (SEED_FINGERPRINT_PEPPER,
REFRESH_TOKEN_PEPPER, IP_SALT). They were missing, so `cp .env.example .env` on a
fresh setup boot-failed closed on the first required-secret check.
- README: correct the first-admin bootstrap to `smirk-admin setup --pubkey`.
create-admin-wallet registers a pending key but does not latch, so on a fresh
instance it leaves the bootstrap uninitialized.
- docs/operations/OPERATOR_SETUP.md: full deployment guide — database, secrets,
per-chain backends, run + verify, admin bootstrap, registration policy, and
smoke-testing an instance (SMOKE_API_STYLE=namespaced).
Surfaced by a clean-room operator setup against a fresh instance.