Gossip
Gossip lets the client select relays for the public keys involved in an operation instead of sending every request to every relay in the pool. It learns read and write relays from NIP-65 lists, inbox relays from NIP-17 lists, and relay hints observed in events.
The gossip store is separate from the event database. It contains relay-selection data and the information needed to refresh it. SQLite is recommended when the platform supports it because that knowledge survives restarts; the in-memory store is useful for Web, tests, and short-lived processes.
Configure the client
Rust provides the stores as separate crates. Add the SQLite store to the existing SDK dependencies:
[dependencies]
nostr-gossip-sqlite = "0.45"
For an ephemeral store, use this dependency instead:
nostr-gossip-memory = "0.45"
use nostr_gossip_sqlite::prelude::NostrGossipSqlite;
use nostr_sdk::prelude::*;
async fn build_gossip_client() -> Result<Client, Box<dyn std::error::Error>> {
let gossip = NostrGossipSqlite::open("./data/gossip.sqlite").await?;
let client = Client::builder().gossip(gossip).build();
client
.add_relay("wss://relay.damus.io")
.capabilities(RelayCapabilities::DISCOVERY)
.await?;
client
.add_relay("wss://purplepag.es")
.capabilities(RelayCapabilities::DISCOVERY)
.await?;
client.connect().await;
Ok(client)
}
from nostr_sdk import (
Client,
ClientBuilder,
NostrGossip,
RelayCapabilities,
RelayUrl,
)
async def build_gossip_client() -> Client:
gossip = await NostrGossip.sqlite("./data/gossip.sqlite")
client = ClientBuilder().gossip(gossip).build()
capabilities = RelayCapabilities.discovery()
await client.add_relay(
RelayUrl.parse("wss://relay.damus.io"), capabilities=capabilities
)
await client.add_relay(
RelayUrl.parse("wss://purplepag.es"), capabilities=capabilities
)
await client.connect()
return client
import {
ClientBuilder,
NostrGossip,
RelayCapabilities,
RelayUrl,
} from "@nostrdevkit/nostr-sdk-node";
async function buildGossipClient() {
const gossip = await NostrGossip.sqlite("./data/gossip.sqlite");
const client = new ClientBuilder().gossip(gossip).build();
const capabilities = RelayCapabilities.discovery();
await client.addRelay(RelayUrl.parse("wss://relay.damus.io"), capabilities, false, undefined);
await client.addRelay(RelayUrl.parse("wss://purplepag.es"), capabilities, false, undefined);
await client.connect(undefined);
return client;
}
import {
ClientBuilder,
NostrGossip,
RelayCapabilities,
RelayUrl,
uniffiInitAsync,
} from "@nostrdevkit/nostr-sdk-web";
async function buildGossipClient() {
await uniffiInitAsync();
const gossip = NostrGossip.inMemory();
const client = new ClientBuilder().gossip(gossip).build();
const capabilities = RelayCapabilities.discovery();
await client.addRelay(RelayUrl.parse("wss://relay.damus.io"), capabilities, false, undefined);
await client.addRelay(RelayUrl.parse("wss://purplepag.es"), capabilities, false, undefined);
await client.connect(undefined);
return client;
}
The Web package does not expose SQLite, so gossip data is rebuilt after a page reload.
import {
ClientBuilder,
NostrGossip,
RelayCapabilities,
RelayUrl,
} from "@nostrdevkit/nostr-sdk-react-native";
export async function buildGossipClient(databasePath: string) {
const gossip = await NostrGossip.sqlite(databasePath);
const client = new ClientBuilder().gossip(gossip).build();
const capabilities = RelayCapabilities.discovery();
await client.addRelay(RelayUrl.parse("wss://relay.damus.io"), capabilities, false, undefined);
await client.addRelay(RelayUrl.parse("wss://purplepag.es"), capabilities, false, undefined);
await client.connect(undefined);
return client;
}
Pass an absolute path inside the application’s data directory as databasePath.
import org.nostrdevkit.sdk.*
suspend fun buildGossipClient(databasePath: String): Client {
val gossip = NostrGossip.sqlite(databasePath)
val client = ClientBuilder().gossip(gossip).build()
val capabilities = RelayCapabilities.discovery()
client.addRelay(RelayUrl.parse("wss://relay.damus.io"), capabilities)
client.addRelay(RelayUrl.parse("wss://purplepag.es"), capabilities)
client.connect()
return client
}
On Android, pass a path inside the application files directory.
import NostrSDK
func buildGossipClient(databasePath: String) async throws -> Client {
let gossip = try await NostrGossip.sqlite(path: databasePath)
let client = ClientBuilder().gossip(gossip: gossip).build()
let capabilities = RelayCapabilities.discovery()
_ = try await client.addRelay(
url: try RelayUrl.parse(url: "wss://relay.damus.io"),
capabilities: capabilities
)
_ = try await client.addRelay(
url: try RelayUrl.parse(url: "wss://purplepag.es"),
capabilities: capabilities
)
await client.connect()
return client
}
Pass a path inside Application Support or another application-owned directory.
namespace Snippets;
using Nostr.Sdk;
public static class GossipExample
{
public static async Task<Client> BuildGossipClient(string databasePath)
{
var gossip = await NostrGossip.Sqlite(databasePath);
var client = new ClientBuilder().Gossip(gossip).Build();
var capabilities = RelayCapabilities.Discovery();
await client.AddRelay(RelayUrl.Parse("wss://relay.damus.io"), capabilities);
await client.AddRelay(RelayUrl.Parse("wss://purplepag.es"), capabilities);
await client.Connect();
return client;
}
}
ClientBuilder.gossip enables gossip routing. The discovery relays are marked with DISCOVERY, so they are used to
refresh relay lists rather than as general read and write relays. Configure more than one discovery relay so one
unavailable service does not prevent updates.
Requests, subscriptions, sync, and publishing continue to use their normal APIs. With automatic targets, filters and events containing public keys allow the client to select the corresponding discovered relays. Explicit or manual targets remain explicit and bypass automatic gossip selection.