docs(relay): packaging (deploy/relay) + operator guide + .env.example

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.
This commit is contained in:
jwinterm
2026-07-02 07:06:36 -04:00
parent f1872c781c
commit 17bb7bcc20
5 changed files with 205 additions and 0 deletions
+7
View File
@@ -110,6 +110,13 @@ self-hosting bypass them. All default off or open.
- **Restore policy** — `WALLET_RESTORE_POLICY=create-only|bounded|unlimited` (default `create-only`), with optional proof-of-work pricing on restore depth.
- **Pay-to-register** — `REGISTRATION_REQUIRE_PAYMENT=true` against a BTCPay-compatible processor (`PAYMENT_*`); a new wallet settles an invoice, paid to your own wallet, before it can register.
## 7. Nostr relay (optional)
Run a first-party, locked-down Nostr relay as your users' encrypted-DM inbox
(`RELAY_ENABLED=true` + `RELAY_*`). Off by default; write-restricted via a gRPC
admission service, advertised through `/capabilities` + NIP-05. Full guide:
[RELAY.md](RELAY.md).
## Testing an instance
The `@smirk/smoke-tests` harness drives the API client end-to-end. Point it at an
+87
View File
@@ -0,0 +1,87 @@
# Nostr relay (messaging plane) — operator guide
The relay is **optional**. It gives your users a first-party, **locked-down**
Nostr relay that acts as their **encrypted-DM inbox** — the transport foundation
for messaging (and, later, P2P coordination). It is **off by default**; a Smirk
backend is fully functional without it.
## What it is (and isn't)
- **It is the user's inbox relay**, advertised via `/capabilities` and NIP-05
relay hints, and **write-restricted** by policy (see below).
- **It is NOT the only relay.** For cross-wallet delivery (e.g. to/from Goblin or
any Nostr user), clients also connect to the public interop relays
(`relay.damus.io`, `nos.lol`). A private relay alone would black-hole DMs to
outsiders — so the NIP-05 hints list your relay **first**, then the public set.
- The backend **does not run the relay process** — it *connects to + advertises*
it and enforces the write policy. You run the relay (bundled or external).
## Architecture
```
wallet ──ws──▶ your relay (nostr-rs-relay) ──gRPC EventAdmit──▶ smirk-backend
(RELAY_WRITE_POLICY
+ NIP-13 PoW,
registered-npub DB)
```
`nostr-rs-relay`'s `[grpc] event_admission_server` calls the backend once per
inbound event; the backend runs the pure policy engine (`src/infra/relay/policy.rs`)
against the user DB and returns PERMIT/DENY. This is dynamic (per-event, per-
recipient), which a static pubkey whitelist can't do.
## Write policies (`RELAY_WRITE_POLICY`)
| policy | who may publish |
| --- | --- |
| `inbox-outbox` (default) | registered npubs publish their own events (outbox); **anyone** may deliver a NIP-17 gift-wrap (kind 1059) **addressed to a registered user** (inbox) |
| `author-allowlist` | only registered Smirk npubs may publish anything (no external inbound) |
| `open` | accept everything (the relay's resource caps still apply) |
`RELAY_INBOUND_POW_BITS` (0 = off) requires a NIP-13 proof-of-work on
cross-ecosystem (non-registered-author) inbound events — spam friction without an
author allowlist. Only the `inbox-outbox` external-inbound branch is gated.
## Enable it
1. Set the relay env (see `.env.example` → *Nostr relay*):
```sh
RELAY_ENABLED=true
RELAY_MODE=bundled # or external
RELAY_URL=wss://relay.yourdomain.tld # what clients connect to (wss in prod)
RELAY_WRITE_POLICY=inbox-outbox
RELAY_INBOUND_POW_BITS=0
RELAY_ADMISSION_BIND=127.0.0.1:8090 # loopback gRPC admission
```
The backend fail-closes at startup on a bad relay config, and for any non-`open`
policy it spawns the gRPC admission service on `RELAY_ADMISSION_BIND`.
2. **Bundled** (`RELAY_MODE=bundled`): run the reference `nostr-rs-relay` in
[`deploy/relay/`](../../deploy/relay/):
```sh
# set relay_url + event_admission_server in deploy/relay/config.toml first
docker compose -f deploy/relay/docker-compose.yml up -d
```
It uses host networking so the relay reaches the loopback admission socket
while that socket stays private (Linux; see the compose comments for other
hosts). Front `RELAY_URL` with your TLS terminator.
3. **External** (`RELAY_MODE=external`): point `RELAY_URL` at any relay you run,
and configure *its* `[grpc] event_admission_server` at `RELAY_ADMISSION_BIND`.
## Verify
- `GET /capabilities` → `features.nostr_relay: true` and a `messaging` object
(`relay_url`, `write_policy`, `inbound_pow_bits`, `supported_nips`).
- `GET /.well-known/nostr.json?name=<user>` lists your relay first in `relays`.
- Publish a registered-npub event → accepted; a random-author, non-gift-wrap
event → rejected (`inbox-outbox`); an under-PoW cross-ecosystem event →
rejected (when `RELAY_INBOUND_POW_BITS > 0`).
## Modularity
`nostr-rs-relay` is the first adapter behind the `RelayProvider` seam
(`src/infra/relay/`). The write policy lives in a pure engine; the gRPC transport
is the adapter's concern. A different relay (strfry, or another messaging
protocol) can enforce the same policy over a different mechanism without touching
the policy or the advertising.