NIP-XX ======
Cluster Replication Protocol
draft optional
This NIP defines an HTTP-based pull replication protocol for relay clusters. It enables relay operators to form distributed networks where relays actively poll each other to synchronize events, providing efficient traffic patterns and improved data availability. Cluster membership is managed by designated cluster administrators who publish membership lists that relays replicate and use to update their polling targets.
Current Nostr relay implementations operate independently, leading to fragmented event storage across the network. Users must manually configure multiple relays to ensure their events are widely available. This creates several problems:
This NIP addresses these issues by enabling relay operators to form clusters that actively replicate events using efficient HTTP polling mechanisms, creating more resilient and bandwidth-efficient event distribution networks.
This NIP defines the following new event kinds:
| Kind | Description |
|---|---|
39108 | Cluster Membership List |
Cluster administrators publish this replaceable event to define the current set of cluster members. All cluster relays replicate this event and update their polling lists when it changes:
{
"kind": 39108,
"content": "{\"name\":\"My Cluster\",\"description\":\"Community relay cluster\"}",
"tags": [
["d", "membership"],
["relay", "https://relay1.example.com/", "wss://relay1.example.com/"],
["relay", "https://relay2.example.com/", "wss://relay2.example.com/"],
["relay", "https://relay3.example.com/", "wss://relay3.example.com/"],
["version", "1"]
],
"pubkey": "<admin-pubkey-hex>",
"created_at": <unix-timestamp>,
"id": "<event-id>",
"sig": "<signature>"
}
Tags:
d: Identifier for the membership list (always "membership")relay: HTTP and WebSocket URLs of cluster member relays (comma-separated)version: Protocol version numberContent: JSON object containing cluster metadata (name, description)
Authorization: Only events signed by cluster administrators are valid for membership updates. Cluster administrators are designated through static relay configuration and cannot be modified by membership events.
Returns the current highest event serial number in the relay's database.
Endpoint: GET /cluster/latest
Response:
{
"serial": 12345678,
"timestamp": 1640995200
}
Parameters:
serial: The highest event serial number in the databasetimestamp: Unix timestamp when this serial was last updatedReturns event IDs for a range of serial numbers.
Endpoint: GET /cluster/events
Query Parameters:
from: Starting serial number (inclusive)to: Ending serial number (inclusive)limit: Maximum number of event IDs to return (default: 1000, max: 10000)Response:
{
"events": [
{
"serial": 12345670,
"id": "abc123...",
"timestamp": 1640995100
},
{
"serial": 12345671,
"id": "def456...",
"timestamp": 1640995110
}
],
"has_more": false,
"next_from": null
}
Response Fields:
events: Array of event objects with serial, id, and timestamphas_more: Boolean indicating if there are more resultsnext_from: Serial number to use as from parameter for next request (if has_more is true)Each relay maintains a replication state for each cluster peer:
/cluster/latest from each peer/cluster/events to get event IDs in the serial range gapEach relay maintains an internal serial number that increments with each stored event:
When fetching events that already exist locally:
Relay A Relay B
| |
|--- User Event ---------->| (Event stored with serial 1001)
| |
| | (5 seconds later)
| |
|<--- GET /cluster/latest --| (A polls B, gets serial 1001)
|--- Response: 1001 ------->|
| |
|<--- GET /cluster/events --| (A fetches event IDs from serial 1000-1001)
|--- Response: [event_id] ->|
| |
|<--- REQ [event_id] ------| (A fetches full event via WebSocket)
|--- EVENT [event_id] ---->|
| |
| (Event stored locally) |
Admin Client Relay A Relay B
| | |
|--- Kind 39108 -------->| (New member added) |
| | |
| |<--- REQ membership ----->| (A subscribes to membership updates)
| |--- EVENT membership ---->|
| | |
| | (A updates polling list)|
| | |
| |<--- GET /cluster/latest -| (A starts polling B)
| | |
/cluster/latest and /cluster/eventsThis NIP is fully backwards compatible:
A reference implementation SHOULD include:
/cluster/latest and /cluster/events{
"kind": 39108,
"content": "{\"name\":\"Test Cluster\",\"description\":\"Development cluster\"}",
"tags": [
["d", "membership"],
["relay", "https://relay1.test.com/", "wss://relay1.test.com/"],
["relay", "https://relay2.test.com/", "wss://relay2.test.com/"],
["version", "1"]
],
"pubkey": "testadminpubkeyhex",
"created_at": 1640995200,
"id": "membership_event_id",
"sig": "membership_event_signature"
}
{
"serial": 12345678,
"timestamp": 1640995200
}
{
"events": [
{
"serial": 12345676,
"id": "event_id_1",
"timestamp": 1640995190
},
{
"serial": 12345677,
"id": "event_id_2",
"timestamp": 1640995195
},
{
"serial": 12345678,
"id": "event_id_3",
"timestamp": 1640995200
}
],
"has_more": false,
"next_from": null
}
This document is placed in the public domain.