Negentropy sync
Finite reads ask relays to send matching events. Sync instead compares the event IDs in the local database with those on each target relay using NIP-77 Negentropy, then transfers only the missing events allowed by the selected direction.
Sync requires a database with a meaningful local event set. Running it against a fresh in-memory client only compares the relay with temporary process state.
Use sync for startup catch-up, reconnect recovery, backup, or reconciliation. Use streaming when events should be processed immediately without maintaining a local replica. The target relay must support NIP-77.
Reconcile a bounded set
The example downloads up to 20 matching text notes into the database.
use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*;
async fn sync_events() -> Result<(), Box<dyn std::error::Error>> {
let database = NostrLmdb::open("./data/nostr").await?;
let client = ClientBuilder::default().database(database).build();
client.add_relay("wss://relay.damus.io").await?;
client.connect().await;
let filter = Filter::new().kind(Kind::TextNote).limit(20);
let opts = SyncOptions::default().direction(SyncDirection::Down);
let output = client.sync(filter).opts(opts).await?;
println!("Sync result: {output:?}");
Ok(())
}
from nostr_sdk import (
ClientBuilder,
Filter,
Kind,
KindStandard,
NostrLmdb,
RelayUrl,
SyncDirection,
SyncOptions,
)
async def sync_events() -> None:
database = await NostrLmdb.open("./data/nostr")
client = ClientBuilder().database(database).build()
await client.add_relay(RelayUrl.parse("wss://relay.damus.io"))
await client.connect()
filter = (
Filter()
.kind(Kind.from_std(KindStandard.TEXT_NOTE))
.limit(20)
)
opts = SyncOptions().direction(SyncDirection.DOWN)
output = await client.sync(filter, opts=opts)
print(output)
import {
ClientBuilder,
Filter,
Kind,
KindStandard,
NostrLmdb,
RelayUrl,
SyncDirection,
SyncOptions,
} from "@nostrdevkit/nostr-sdk-node";
async function syncEvents() {
const database = await NostrLmdb.open("./data/nostr");
const client = new ClientBuilder().database(database).build();
await client.addRelay(
RelayUrl.parse("wss://relay.damus.io"),
undefined,
false,
undefined,
);
await client.connect(undefined);
const filter = new Filter()
.kind(Kind.fromStd(KindStandard.TextNote))
.limit(20n);
const opts = new SyncOptions().direction(SyncDirection.Down);
const output = await client.sync(filter, undefined, opts);
console.log(output);
}
The Web package does not bundle a persistent database. Implement NostrDatabase over browser storage and pass it to
ClientBuilder.database before using sync. An in-memory sync is intentionally not shown because it does not provide a
durable local replica.
import {
ClientBuilder,
Filter,
Kind,
KindStandard,
NostrLmdb,
RelayUrl,
SyncDirection,
SyncOptions,
} from "@nostrdevkit/nostr-sdk-react-native";
export async function syncEvents(databasePath: string) {
const database = await NostrLmdb.open(databasePath);
const client = new ClientBuilder().database(database).build();
await client.addRelay(
RelayUrl.parse("wss://relay.damus.io"),
undefined,
false,
undefined,
);
await client.connect(undefined);
const filter = new Filter()
.kind(Kind.fromStd(KindStandard.TextNote))
.limit(20n);
const opts = new SyncOptions().direction(SyncDirection.Down);
const output = await client.sync(filter, undefined, opts);
console.log(output);
}
import org.nostrdevkit.sdk.*
suspend fun syncEvents() {
val database = NostrLmdb.open("./data/nostr")
val client = ClientBuilder().database(database).build()
client.addRelay(RelayUrl.parse("wss://relay.damus.io"))
client.connect()
val filter = Filter()
.kind(Kind.fromStd(KindStandard.TEXT_NOTE))
.limit(20u)
val opts = SyncOptions().direction(SyncDirection.DOWN)
val output = client.sync(filter, opts = opts)
println(output)
}
import Foundation
import NostrSDK
func syncEvents(path: String) async throws {
let database = try await NostrLmdb.open(path: path)
let client = ClientBuilder().database(database: database).build()
_ = try await client.addRelay(
url: try RelayUrl.parse(url: "wss://relay.damus.io")
)
await client.connect()
let filter = Filter()
.kind(kind: Kind.fromStd(e: .textNote))
.limit(limit: 20)
let opts = SyncOptions().direction(direction: .down)
let output = try await client.sync(filter: filter, opts: opts)
print(output)
}
Pass a URL-derived path inside Application Support or another application-owned directory.
using Nostr.Sdk;
public static class DatabaseExample
{
public static async Task SyncEvents(string path)
{
var database = await NostrLmdb.Open(path);
var client = new ClientBuilder().Database(database).Build();
await client.AddRelay(RelayUrl.Parse("wss://relay.damus.io"));
await client.Connect();
var filter = new Filter()
.Kind(Kind.FromStd(KindStandard.TextNote))
.Limit(20);
var opts = new SyncOptions().Direction(SyncDirection.Down);
var output = await client.Sync(filter, opts: opts);
Console.WriteLine(output);
}
}
The example selects downward sync so it cannot publish local events. Adjust the filter’s authors, kinds, tags, and limit to match the data set the application needs locally.
After sync, inspect the returned relay outcome before treating the local view as reconciled, then query the database. Add a live subscription when new events must continue arriving after sync ends. The current direction, targeting, timeout, progress, and result APIs belong in the references collected in Where to go next, rather than being duplicated here.