Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

outlay

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. See design/design.md for the locked design and design/shim.md for the shim.


Run

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/outlay

Prebuilt binary (linux/amd64 or linux/arm64, from Releases):

tar xzf outlay-amd64.tar.gz # or outlay-arm64.tar.gz
./outlay

Build from source (Rust ≥ 1.88):

cargo run

On 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/outlay

Connect a client

outlay 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.wtf by default), target the server pubkey outlay printed, and call subscribe / 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>

Try it

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>

How it works

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, a nostr-sdkClient connected to the upstream. By default that's the bundled in-process relay (loopback); with OUTLAY_PROXY_RELAY_URL set, it's that external relay. Published events are forwarded verbatim (client-signed), never re-signed.
  • CVM transport — the NostrServerTransport that CVM clients connect through, on the ContextVM relays you configure.

Configuration

Loaded from .env then .env.local (first-write-wins per key), then the process environment.

VariableDefaultDescription
OUTLAY_PROXY_RELAY_URL(unset → bundled)External upstream to proxy. Unset = run the bundled relay (default).
OUTLAY_CVM_RELAYSwss://nostr.wtfComma-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_NAMEoutlayCVM profile name.
OUTLAY_ANNOUNCEDfalsePublic discovery (kind 11316) on/off.
OUTLAY_BUNDLED_BACKENDsqliteBundled relay backend: sqlite (persistent) or memory (volatile).
OUTLAY_BUNDLED_DB_PATHoutlay-relay.dbSQLite path (ignored for memory).
OUTLAY_BUNDLED_PORT0Bundled relay bind port (0 = scan a free loopback port).

CVM tool surface

  • subscribe(subscription_id, filters) — streaming. Opens a NIP-01 subscription upstream and streams EVENT/EOSE/CLOSED chunks. Cancel by aborting the call (= NIP-01 CLOSE).
  • publish_event(event) — synchronous. Forwards a client-signed event verbatim; returns { ok, event_id, message } mirroring the upstream OK.
  • 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 under upstream and 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 — vanilla NIP-01 bridge

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
VariableDefaultDescription
OUTLAY_SHIM_LISTEN_ADDR127.0.0.1:8088Address to listen on (the Docker image sets 0.0.0.0:8088).
OUTLAY_SHIM_RELAYtrueRun the colocated memoryless NIP-01 relay at / (see below).
OUTLAY_SHIM_CVM_RELAYSwss://nostr.wtfComma-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_TIMEOUT15 (seconds)CVM transport handshake timeout.
OUTLAY_SHIM_PRIVATE_KEY(ephemeral)Hex/nsec shim key.
OUTLAY_SHIM_ENCRYPTION_MODEoptionalCVM transport encryption: disabled / optional / required.
OUTLAY_SHIM_GIFT_WRAP_MODEephemeralOutbound gift-wrap kind: ephemeral (21059) / persistent (1059) / optional.
OUTLAY_SHIM_MAX_CACHED_OUTLAYS64Max distinct outlay identities cached (each holds one CVM transport).
OUTLAY_SHIM_MAX_WS_MESSAGE_BYTES1048576 (1 MiB)WS frame/message size limit.

Colocated relay at / (the "collapse")

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_RELAYSmust 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).

Testing

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 warnings

Releases

Binaries (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>, push

Project layout

outlay/
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

Roadmap

  • Authzallowed_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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages