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

Databases

The client database is the local event store used by queries and reconciliation. It indexes events by Nostr filters and applies protocol storage rules such as replacement and deletion.

Configure a persistent backend before building the client when local history must survive restarts or be used by Negentropy sync.

Use LMDB

Rust provides LMDB through the separate nostr-lmdb crate. The native bindings include NostrLmdb in the nostr-sdk package. Other Rust backends are listed in the Nostr repository.

The usual setup is to open one database for an application-owned path, pass it to ClientBuilder, and query it through the client. Binding-specific differences are noted in their tabs.

Rust

Add the backend alongside nostr-sdk:

[dependencies]
nostr-lmdb = "0.45"
use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*;

async fn query_database() -> Result<(), Box<dyn std::error::Error>> {
    let database = NostrLmdb::open("./data/nostr").await?;
    let client = ClientBuilder::default().database(database).build();

    let filter = Filter::new().kind(Kind::TextNote).limit(20);
    let events = client.database().query(filter).await?;
    println!("Found {} stored events", events.len());

    Ok(())
}
Python
from nostr_sdk import (
    ClientBuilder,
    Filter,
    Kind,
    KindStandard,
    NostrLmdb,
    RelayUrl,
    SyncDirection,
    SyncOptions,
)

async def query_database() -> None:
    database = await NostrLmdb.open("./data/nostr")
    client = ClientBuilder().database(database).build()

    filter = Filter().kind(Kind.from_std(KindStandard.TEXT_NOTE)).limit(20)
    events = await client.database().query(filter)
    print(f"Found {len(events)} stored events")

JavaScript
Node.js
import {
    ClientBuilder,
    Filter,
    Kind,
    KindStandard,
    NostrLmdb,
    RelayUrl,
    SyncDirection,
    SyncOptions,
} from "@nostrdevkit/nostr-sdk-node";

async function queryDatabase() {
    const database = await NostrLmdb.open("./data/nostr");
    const client = new ClientBuilder().database(database).build();

    const filter = new Filter()
        .kind(Kind.fromStd(KindStandard.TextNote))
        .limit(20n);
    const events = await client.database().query(filter);
    console.log(`Found ${events.length} stored events`);
}
Web

The Web package exposes the database interface but does not bundle a persistent backend. Implement NostrDatabase over browser storage and pass it to ClientBuilder.database; the default in-memory store is not a replacement for durable browser data.

React Native
import {
    ClientBuilder,
    Filter,
    Kind,
    KindStandard,
    NostrLmdb,
    RelayUrl,
    SyncDirection,
    SyncOptions,
} from "@nostrdevkit/nostr-sdk-react-native";

export async function queryDatabase(databasePath: string) {
    const database = await NostrLmdb.open(databasePath);
    const client = new ClientBuilder().database(database).build();

    const filter = new Filter()
        .kind(Kind.fromStd(KindStandard.TextNote))
        .limit(20n);
    const events = await client.database().query(filter);
    console.log(`Found ${events.length} stored events`);
}

Pass an absolute path inside the application’s data directory as databasePath.

Kotlin
import org.nostrdevkit.sdk.*

suspend fun queryDatabase() {
    val database = NostrLmdb.open("./data/nostr")
    val client = ClientBuilder().database(database).build()

    val filter = Filter()
        .kind(Kind.fromStd(KindStandard.TEXT_NOTE))
        .limit(20u)
    val events = client.database().query(filter)
    println("Found ${events.size} stored events")
}

On Android, use a path inside the application files directory rather than a relative path.

Swift
import Foundation
import NostrSDK

func queryDatabase(path: String) async throws {
    let database = try await NostrLmdb.open(path: path)
    let client = ClientBuilder().database(database: database).build()

    let filter = Filter()
        .kind(kind: Kind.fromStd(e: .textNote))
        .limit(limit: 20)
    let events = try await client.database().query(filter: filter)
    print("Found \(events.count) stored events")
}

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 QueryDatabase(string path)
    {
        var database = await NostrLmdb.Open(path);
        var client = new ClientBuilder().Database(database).Build();

        var filter = new Filter()
            .Kind(Kind.FromStd(KindStandard.TextNote))
            .Limit(20);
        var events = await client.Database().Query(filter);
        Console.WriteLine($"Found {events.Length} stored events");
    }
}

client.database().query() is a local operation. It does not contact relays and an empty result says only that no matching event is currently stored. Use a finite request when the feature needs a fresh network read, or sync before querying when the local store is the application’s read model.

How events reach the store

Validated events received through client requests and subscriptions are available to the configured database. Sync also writes events downloaded from relays. Applications may call save_event directly for an already verified event, but that method assumes verification has happened; do not use it as an ingestion path for untrusted wire data.

Database filters use the same Filter type as relay requests, but a local query has no network freshness or relay provenance. Use a relay request for fresh remote data, or sync first when the database is the application’s local view.