Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Rust
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(())
}
Python
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)

JavaScript
Node.js
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);
}
Web

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.

React Native
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);
}
Kotlin
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)
}
Swift
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.

C#
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.