A Nostr relay exposed as a ContextVM (CVM) server. outlay binds CVM tool
calls to NIP-01 relay traffic: a CVM client calls subscribe /
publish_event / relay_info, and outlay translates each call into the
corresponding NIP-01 exchange, streaming relay events back over
CEP-41 open-stream.
It just runs. With no configuration, outlay starts a bundled in-process Nostr relay as its upstream — a working, persistent (SQLite) relay the moment it boots. Point it at any other relay instead with a single env var.
Status: v1 —
outlay(bundled-relay default + external-upstream proxy mode),outlay-shim(vanilla-NIP-01 bridge), and the release pipeline are done and tested. Seedesign/design.mdfor the locked design anddesign/shim.mdfor the shim.
outlay is self-contained: no config means the bundled relay runs as the upstream. Pick any install path — all three are zero-config.
Docker (no build; the volume persists the relay's SQLite across restarts):
docker run --rm -v outlay-data:/data ghcr.io/contextvm/outlayPrebuilt binary (linux/amd64 or linux/arm64, from Releases):
tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlayBuild from source (Rust ≥ 1.88):
cargo runOn startup outlay logs its server pubkey, the CVM relays it listens on, the
upstream, and mode=bundled. Copy that pubkey — it's how clients address
the server.
Proxy an external relay instead (advanced) — reach any relay rather than the bundled one:
OUTLAY_PROXY_RELAY_URL=wss://relay.primal.net cargo run
# or: docker run --rm -e OUTLAY_PROXY_RELAY_URL=wss://relay.primal.net ghcr.io/contextvm/outlayoutlay has no inbound port — it connects out to the CVM relays, and clients reach it there. Two ways in:
- CVM client → connect to the CVM relay (
wss://nostr.wtfby default), target the server pubkey outlay printed, and callsubscribe/publish_event/relay_info. - Vanilla Nostr client (gossip,
nak, web wallets) → doesn't speak CVM, so run the shim and point the client at it:cargo run -p outlay-shim # then connect the client to ws://localhost:8088/<server-pubkey>
With outlay running (cargo run), in another terminal publish a note to it
through the shim, then read it back:
# 1. Start the shim (bridges vanilla NIP-01 → outlay over CVM):
cargo run -p outlay-shim
# 2. Publish a text note via the shim (<server-pubkey> = what outlay printed):
nak event -c "hello from outlay" ws://localhost:8088/<server-pubkey>
# 3. Read it back:
nak req -k 1 -l 1 ws://localhost:8088/<server-pubkey>A CVM server is an rmcp handler run over NostrServerTransport — its surface is
MCP tools, not raw WebSocket frames. So "a relay over CVM" means the tool
surface and streamed payload mirror NIP-01's message shapes, with CEP-41
open-stream carrying the relay→client direction. Each open-stream chunk is one
verbatim NIP-01 relay→client JSON array.
The core mapping: one CEP-41 stream == one NIP-01 subscription.
| NIP-01 (relay) | CVM (outlay) |
|---|---|
["REQ", sub, filters] |
tools/call subscribe{subscription_id, filters} + progressToken |
["EVENT", sub, e] |
open-stream chunk ["EVENT","sub",{event}] |
["EOSE", sub] |
open-stream chunk ["EOSE","sub"] |
["CLOSED", sub, msg] |
open-stream chunk ["CLOSED","sub","msg"] |
["CLOSE", sub] |
client aborts the stream (call.abort()) |
["EVENT", e] (publish) |
tools/call publish_event{event} → {ok, event_id, message} |
CVM client ──── CVM tools over Nostr ──── outlay server ──── NIP-01 ws ──── upstream
(CEP-41 open-stream) (proxy + rmcp) (bundled relay
or any external relay)
Two independent relay connections live inside outlay:
- Upstream pool — outlay's own
Proxy, anostr-sdkClientconnected to the upstream. By default that's the bundled in-process relay (loopback); withOUTLAY_PROXY_RELAY_URLset, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed. - CVM transport — the
NostrServerTransportthat CVM clients connect through, on the ContextVM relays you configure.
Loaded from .env then .env.local (first-write-wins per key), then the
process environment.
| Variable | Default | Description |
|---|---|---|
OUTLAY_PROXY_RELAY_URL |
(unset → bundled) | External upstream to proxy. Unset = run the bundled relay (default). |
OUTLAY_CVM_RELAYS |
wss://nostr.wtf |
Comma-separated CVM transport relays the server listens on (distinct from OUTLAY_PROXY_RELAY_URL, the upstream being proxied). |
OUTLAY_SERVER_PRIVATE_KEY |
(ephemeral) | Hex/nsec server key. Unset → new key each start. |
OUTLAY_SERVER_NAME |
outlay |
CVM profile name. |
OUTLAY_ANNOUNCED |
false |
Public discovery (kind 11316) on/off. |
OUTLAY_BUNDLED_BACKEND |
sqlite |
Bundled relay backend: sqlite (persistent) or memory (volatile). |
OUTLAY_BUNDLED_DB_PATH |
outlay-relay.db |
SQLite path (ignored for memory). |
OUTLAY_BUNDLED_PORT |
0 |
Bundled relay bind port (0 = scan a free loopback port). |
subscribe(subscription_id, filters)— streaming. Opens a NIP-01 subscription upstream and streamsEVENT/EOSE/CLOSEDchunks. Cancel by aborting the call (= NIP-01CLOSE).publish_event(event)— synchronous. Forwards a client-signed event verbatim; returns{ ok, event_id, message }mirroring the upstreamOK.relay_info()— synchronous. Fetches the upstream's NIP-11 document over HTTP and overlays outlay's identity (software/version/proxy); the upstream's identity is preserved underupstreamand all other fields pass through verbatim. Falls back to a synthesized minimum when the upstream serves no NIP-11 (notably the bundled relay).
outlay-shim is a WebSocket/HTTP endpoint that translates vanilla NIP-01
(REQ/EVENT/CLOSE) into outlay's CVM tool calls, so ordinary Nostr clients
can reach CVM-exposed relays without speaking CVM. It also hosts a colocated
memoryless NIP-01 relay at / (on by default) that outlays can use as their
CVM transport relay — see Colocated relay at / below. The bridge is
path-keyed: ws://<host>:<port>/<server-pubkey-or-nprofile> (hex, npub, or
nprofile; an nprofile's relay hint overrides the configured CVM relays). Design
in design/shim.md.
cargo run -p outlay-shim| Variable | Default | Description |
|---|---|---|
OUTLAY_SHIM_LISTEN_ADDR |
127.0.0.1:8088 |
Address to listen on (the Docker image sets 0.0.0.0:8088). |
OUTLAY_SHIM_RELAY |
true |
Run the colocated memoryless NIP-01 relay at / (see below). |
OUTLAY_SHIM_CVM_RELAYS |
wss://nostr.wtf |
Comma-separated CVM transport relays used to find outlay servers. With the colocated relay on, this must be the shim's own public URL(s). |
OUTLAY_SHIM_PUBLIC_URLS |
(unset → CVM_RELAYS) |
Public URL(s) this shim is reachable at. Feeds the loopback shortcut, the CLI banner link, and the NIP-11 HTML "Open in Jumble" button. Defaults to OUTLAY_SHIM_CVM_RELAYS. |
OUTLAY_SHIM_CONNECT_TIMEOUT |
15 (seconds) |
CVM transport handshake timeout. |
OUTLAY_SHIM_PRIVATE_KEY |
(ephemeral) | Hex/nsec shim key. |
OUTLAY_SHIM_ENCRYPTION_MODE |
optional |
CVM transport encryption: disabled / optional / required. |
OUTLAY_SHIM_GIFT_WRAP_MODE |
ephemeral |
Outbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional. |
OUTLAY_SHIM_MAX_CACHED_OUTLAYS |
64 |
Max distinct outlay identities cached (each holds one CVM transport). |
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES |
1048576 (1 MiB) |
WS frame/message size limit. |
By default the shim also serves a memoryless (storage-less) NIP-01 relay at /
(OUTLAY_SHIM_RELAY=false disables it). An outlay can point its CVM transport
at the shim's own public URL (OUTLAY_CVM_RELAYS=wss://<shim-host>),
collapsing the transport relay into the shim — one fewer hop and no third-party
dependency. Vanilla clients keep connecting at /<server-pubkey>; / is the
relay.
Loopback shortcut. When the colocated relay is on, the bridge never dials
the shim's own public URL to reach an outlay — that hairpins through the reverse
proxy and times out on most deploys. Instead it rewrites any matching relay URL
(from an nprofile hint or the configured fallback) to the relay's loopback
address. The match set is OUTLAY_SHIM_PUBLIC_URLS, defaulting to
OUTLAY_SHIM_CVM_RELAYS when unset, so the standard deployment needs no extra
config. Third-party relays pass through untouched, so nprofile hints to other
relays keep working.
Config rule. With the colocated relay on, OUTLAY_SHIM_CVM_RELAYS
must be this shim's own public URL — the default above treats it as "self".
To use a third-party transport relay instead, set OUTLAY_SHIM_RELAY=false
(which also disables the shortcut); then OUTLAY_SHIM_CVM_RELAYS may point
anywhere. Running the colocated relay on while pointing CVM_RELAYS at a
third-party relay silently breaks (the bridge loops back to a relay the outlay
isn't on).
cargo test --workspace # unit tests (default = bundled)
cargo test -p outlay --no-default-features # proxy-only config path
cargo test -p outlay --features test-utils --test smoke_bundled # network-free E2E (bundled relay)
cargo test --features test-utils --test smoke -- --ignored --nocapture # real network (primal)
cargo fmt --all && cargo clippy --workspace --all-targets -- -D warningsBinaries (linux/amd64 + linux/arm64) and multi-arch Docker images are published
on every v* tag — see Releases
and ghcr.io/contextvm/outlay · ghcr.io/contextvm/outlay-shim. The Makefile
- GitHub Actions drive it:
make version # print the current shared version
make release # tag the CURRENT version + push (inaugural / re-release)
make patch # or minor / major → bump, commit, tag v<ver>, pushoutlay/
Cargo.toml workspace root (members: crates/*)
crates/
outlay/ the CVM↔NIP-01 relay proxy server (bin+lib; bundled relay default)
src/ config.rs · handler.rs · proxy.rs · main.rs · lib.rs
tests/ smoke.rs (network, #[ignore]) · smoke_bundled.rs (network-free)
outlay-shim/ vanilla NIP-01 client bridge (bin+lib; design/shim.md)
src/ server.rs · conn.rs · translate.rs · nip11.rs · path.rs · transport.rs
outlay-relay/ bundled in-process relay on nostr-sdk 0.45-alpha's LocalRelay
design/ design.md (server) · shim.md (shim)
reference/ gitignored, read-only vendored references (cordn-rs, nostr,
nostr-rs-relay, rs-sdk, nips) — not required to build
- Authz —
allowed_public_keys. Deferred until the shim clarifies the trust model; outlay is an open proxy meanwhile. - Shape B relay — expose the bundled relay on a configurable bind (not just loopback), which forces the authz decision.
- Multi-relay fan-in; NIP-42 AUTH brokering; a NIP-11 cache.
reference/ holds read-only, gitignored copies of the projects this builds on:
rs-sdk (CVM Rust SDK),
cordn-rs (the streaming-CVM pattern
outlay mirrors), and the nostr library.
MIT.