draft optional
This NIP defines a protocol for exposing a private Nostr relay through a public relay, enabling access to relays behind NAT, firewalls, or on devices without public IP addresses. It uses end-to-end encrypted events to tunnel standard Nostr protocol messages through a rendezvous relay.
Users want to run personal relays for:
However, personal relays often run:
NRC solves this by tunneling Nostr protocol messages through encrypted events on a public relay, similar to how NIP-47 tunnels wallet operations.
| Kind | Name | Description |
|---|---|---|
| 24891 | NRC Request | Ephemeral, client→relay wrapped message |
| 24892 | NRC Response | Ephemeral, relay→client wrapped message |
The connection URI format is:
nostr+relayconnect://<relay-pubkey>?relay=<rendezvous-relay>&secret=<client-secret>[&name=<device-name>]
Parameters:
relay-pubkey: The public key of the private relay (64-char hex)relay: The WebSocket URL of the rendezvous relay (URL-encoded)secret: A 32-byte hex-encoded secret used to derive the conversation keyname (optional): Human-readable device identifier for managementExample:
nostr+relayconnect://a1b2c3d4e5f6...?relay=wss%3A%2F%2Frelay.example.com&secret=0123456789abcdef...&name=phone
For privacy-preserving access, NRC supports Cashu Access Tokens (CAT) instead of static secrets:
nostr+relayconnect://<relay-pubkey>?relay=<rendezvous-relay>&auth=cat&mint=<mint-url>
When using CAT authentication:
nrccashu tag┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────────┐
│ Client │────▶│ Public Relay│────▶│ Bridge │────▶│Private Relay│
│ │◀────│ (rendezvous)│◀────│ │◀────│ │
└─────────┘ └─────────────┘ └─────────┘ └─────────────┘
│ │
└────────── NIP-44 encrypted ────────┘
p tags (cannot decrypt content){
"kind": 24891,
"content": "<nip44_encrypted_json>",
"tags": [
["p", "<relay_pubkey>"],
["encryption", "nip44_v2"],
["session", "<session_id>"]
],
"pubkey": "<client_pubkey>",
"created_at": <unix_timestamp>,
"sig": "<signature>"
}
With CAT authentication, add:
["cashu", "cashuA..."]
The encrypted content structure:
{
"type": "EVENT" | "REQ" | "CLOSE" | "AUTH" | "COUNT",
"payload": <standard_nostr_message_array>
}
Where payload is the standard Nostr message array, e.g.:
["EVENT", <event_object>]["REQ", "<sub_id>", <filter1>, <filter2>, ...]["CLOSE", "<sub_id>"]["AUTH", <auth_event>]["COUNT", "<sub_id>", <filter1>, ...]{
"kind": 24892,
"content": "<nip44_encrypted_json>",
"tags": [
["p", "<client_pubkey>"],
["encryption", "nip44_v2"],
["session", "<session_id>"],
["e", "<request_event_id>"]
],
"pubkey": "<relay_pubkey>",
"created_at": <unix_timestamp>,
"sig": "<signature>"
}
The encrypted content structure:
{
"type": "EVENT" | "OK" | "EOSE" | "NOTICE" | "CLOSED" | "COUNT" | "AUTH" | "CHUNK",
"payload": <standard_nostr_response_array>
}
Where payload is the standard Nostr response array, e.g.:
["EVENT", "<sub_id>", <event_object>]["OK", "<event_id>", <success_bool>, "<message>"]["EOSE", "<sub_id>"]["NOTICE", "<message>"]["CLOSED", "<sub_id>", "<message>"]["COUNT", "<sub_id>", {"count": <n>}]["AUTH", "<challenge>"][<chunk_object>] (for CHUNK type, see Message Segmentation)The session tag groups related request/response events, enabling:
Session IDs SHOULD be randomly generated UUIDs or 32-byte hex strings.
All content is encrypted using NIP-44 v2.
The conversation key is derived from:
Some Nostr events exceed the typical relay message size limits (commonly 64KB). NRC supports message segmentation to handle large payloads by splitting them into multiple chunks.
Senders SHOULD chunk messages when the JSON-serialized response exceeds 40KB. This threshold accounts for:
When a response is too large, it is split into multiple CHUNK responses:
{
"type": "CHUNK",
"payload": [{
"type": "CHUNK",
"messageId": "<uuid>",
"index": 0,
"total": 3,
"data": "<base64_encoded_chunk>"
}]
}
Fields:
messageId: A unique identifier (UUID) for the chunked message, used to correlate chunksindex: Zero-based chunk index (0, 1, 2, ...)total: Total number of chunks in this messagedata: Base64-encoded segment of the original messagemessageId (UUID recommended)Example encoding (JavaScript):
const encoded = btoa(unescape(encodeURIComponent(jsonString)))
messageIdindexchunks.size === total):a. Concatenate chunk data in index order (0, 1, 2, ...) b. Base64 decode the concatenated string c. Parse as UTF-8 JSON to recover the original response
Example decoding (JavaScript):
const jsonString = decodeURIComponent(escape(atob(concatenatedBase64)))
const response = JSON.parse(jsonString)
Receivers MUST implement chunk buffer cleanup:
secret parameter in the URInrccashu tag of request eventsSecret-based: Remove the client's derived pubkey from the authorized list.
CAT-based: Remove the client's Nostr pubkey from the ACL. Takes effect immediately due to re-authorization on each request.
#p filter for client's pubkeye tag (references request event ID)messageId#p filter for relay's pubkey