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

Live subscriptions

A subscription remains active and delivers matching events until it is closed. Open the notification stream before subscribing so an event arriving immediately after the request cannot be missed by the consumer.

Use this shape when the application needs future events continuously. For a bounded read of stored events, use a finite event stream instead.

A subscription is pool state, not merely an iterator returned to one function. The client records it, sends it to eligible relays, restores it after reconnection where possible, and emits matching events through the shared notification stream. The returned subscription ID is how application code identifies that state later.

Avoid the startup race

Client notifications are observed from the moment a notification stream is created; they are not a replay log. The safe order is therefore:

  1. create the notification stream;
  2. build a bounded filter and request target;
  3. subscribe and retain the returned subscription ID;
  4. consume notifications and select events for that ID;
  5. unsubscribe during teardown.

Reversing the first two runtime operations can lose an event that arrives after the relay accepts the subscription but before the application starts observing notifications.

This order protects the local handoff between subscription setup and consumption. If a feature needs a complete initial view followed by updates, design an overlap:

  1. Start observing notifications.
  2. Subscribe with a since boundary that overlaps the intended initial history.
  3. Fetch or stream the bounded initial history.
  4. Merge both paths by event ID and order them using feature semantics.
  5. Continue with live notifications.

The overlap trades possible duplicates for avoiding a gap. Event IDs make duplicates straightforward to remove; recovering an event that was never requested is harder.

Rust
use nostr_sdk::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::default();
    client.add_relay("wss://relay.damus.io").await?;
    client.connect().await;

    let mut notifications = client.notifications();
    let filter = Filter::new().kind(Kind::TextNote).since(Timestamp::now());
    let subscription = client.subscribe(filter).await?;

    while let Some(notification) = notifications.next().await {
        if let ClientNotification::Event {
            relay_url,
            subscription_id,
            event,
        } = notification
        {
            if &subscription_id == subscription.id() {
                println!("Received {} from {relay_url}", event.id);
                break;
            }
        }
    }

    client.unsubscribe(subscription.id()).await?;
    Ok(())
}
Python
import asyncio

from nostr_sdk import (
    Client,
    ClientNotification,
    Filter,
    Kind,
    KindStandard,
    RelayUrl,
    ReqTarget,
    Timestamp,
)


async def main() -> None:
    client = Client()
    await client.add_relay(RelayUrl.parse("wss://relay.damus.io"))
    await client.connect()

    notifications = client.notifications()
    filter = (
        Filter()
        .kind(Kind.from_std(KindStandard.TEXT_NOTE))
        .since(Timestamp.now())
    )
    subscription = await client.subscribe(ReqTarget.auto([filter]))

    while notification := await notifications.next():
        if isinstance(notification, ClientNotification.NEW_EVENT):
            if notification.subscription_id == subscription.id:
                print(
                    f"Received {notification.event.id().to_bech32()} "
                    f"from {notification.relay_url}"
                )
                break

    await client.unsubscribe(subscription.id)


if __name__ == "__main__":
    asyncio.run(main())
JavaScript
Node.js
import {
    Client,
    ClientNotification,
    Filter,
    Kind,
    KindStandard,
    RelayUrl,
    ReqTarget,
    Timestamp,
} from "@nostrdevkit/nostr-sdk-node";

async function main() {
    const client = new Client();
    await client.addRelay(RelayUrl.parse("wss://relay.damus.io"));
    await client.connect();

    const notifications = client.notifications();
    const filter = new Filter()
        .kind(Kind.fromStd(KindStandard.TextNote))
        .since(Timestamp.now());
    const subscription = await client.subscribe(ReqTarget.auto([filter]));

    while (true) {
        const notification = await notifications.next();
        if (!notification) break;

        if (ClientNotification.NewEvent.instanceOf(notification)) {
            if (notification.inner.subscriptionId === subscription.id) {
                console.log(
                    `Received ${notification.inner.event.id().toBech32()} ` +
                    `from ${notification.inner.relayUrl}`,
                );
                break;
            }
        }
    }

    await client.unsubscribe(subscription.id);
}

await main();
Web
import {
    Client,
    ClientNotification,
    Filter,
    Kind,
    KindStandard,
    RelayUrl,
    ReqTarget,
    Timestamp,
    uniffiInitAsync,
} from "@nostrdevkit/nostr-sdk-web";

async function main() {
    await uniffiInitAsync();

    const client = new Client();
    await client.addRelay(RelayUrl.parse("wss://relay.damus.io"));
    await client.connect();

    const notifications = client.notifications();
    const filter = new Filter()
        .kind(Kind.fromStd(KindStandard.TextNote))
        .since(Timestamp.now());
    const subscription = await client.subscribe(ReqTarget.auto([filter]));

    while (true) {
        const notification = await notifications.next();
        if (!notification) break;

        if (ClientNotification.NewEvent.instanceOf(notification)) {
            if (notification.inner.subscriptionId === subscription.id) {
                console.log(
                    `Received ${notification.inner.event.id().toBech32()} ` +
                    `from ${notification.inner.relayUrl}`,
                );
                break;
            }
        }
    }

    await client.unsubscribe(subscription.id);
}

await main();
React Native
import {
    Client,
    ClientNotification,
    Filter,
    Kind,
    KindStandard,
    RelayUrl,
    ReqTarget,
    Timestamp,
} from "@nostrdevkit/nostr-sdk-react-native";

export async function receiveLiveEvent() {
    const client = new Client();
    await client.addRelay(RelayUrl.parse("wss://relay.damus.io"));
    await client.connect();

    const notifications = client.notifications();
    const filter = new Filter()
        .kind(Kind.fromStd(KindStandard.TextNote))
        .since(Timestamp.now());
    const subscription = await client.subscribe(ReqTarget.auto([filter]));

    while (true) {
        const notification = await notifications.next();
        if (!notification) break;

        if (ClientNotification.NewEvent.instanceOf(notification)) {
            if (notification.inner.subscriptionId === subscription.id) {
                console.log(
                    `Received ${notification.inner.event.id().toBech32()} ` +
                    `from ${notification.inner.relayUrl}`,
                );
                break;
            }
        }
    }

    await client.unsubscribe(subscription.id);
}
Kotlin
package rust.nostr.snippets

import kotlinx.coroutines.runBlocking
import org.nostrdevkit.sdk.*

suspend fun receiveLiveEvent() {
    val client = Client()
    client.addRelay(RelayUrl.parse("wss://relay.damus.io"))
    client.connect()

    val notifications = client.notifications()
    val filter = Filter()
        .kind(Kind.fromStd(KindStandard.TEXT_NOTE))
        .since(Timestamp.now())
    val subscription = client.subscribe(ReqTarget.auto(listOf(filter)))

    while (true) {
        when (val notification = notifications.next() ?: break) {
            is ClientNotification.NewEvent -> {
                if (notification.subscriptionId == subscription.id) {
                    println("Received ${notification.event.id().toBech32()} from ${notification.relayUrl}")
                    break
                }
            }
            else -> Unit
        }
    }

    client.unsubscribe(subscription.id)
}

fun main() = runBlocking {
    receiveLiveEvent()
}
Swift
import Foundation
import NostrSDK

func receiveLiveEvent() async throws {
    let client = Client()
    let relay = try RelayUrl.parse(url: "wss://relay.damus.io")
    _ = try await client.addRelay(url: relay)
    await client.connect()

    let notifications = client.notifications()
    let filter = Filter()
        .kind(kind: Kind.fromStd(e: .textNote))
        .since(timestamp: Timestamp.now())
    let subscription = try await client.subscribe(
        target: ReqTarget.auto(filters: [filter])
    )

    while let notification = await notifications.next() {
        if case let .newEvent(relayUrl, subscriptionId, event) = notification,
           subscriptionId == subscription.id {
            print("Received \(try event.id().toBech32()) from \(relayUrl)")
            break
        }
    }

    _ = try await client.unsubscribe(subscriptionId: subscription.id)
}
C#
namespace Snippets;

using Nostr.Sdk;

public static class LiveExample
{
    public static async Task ReceiveLiveEvent()
    {
        var client = new Client();
        await client.AddRelay(RelayUrl.Parse("wss://relay.damus.io"));
        await client.Connect();

        ClientNotificationStream notifications = client.Notifications();
        Filter filter = new Filter()
            .Kind(Kind.FromStd(KindStandard.TextNote))
            .Since(Timestamp.Now());
        SubscribeOutput subscription = await client.Subscribe(ReqTarget.Auto([filter]));

        while (await notifications.Next() is { } notification)
        {
            if (notification is ClientNotification.NewEvent item &&
                item.SubscriptionId == subscription.Id)
            {
                Console.WriteLine($"Received {item.Event.Id().ToBech32()} from {item.RelayUrl}");
                break;
            }
        }

        await client.Unsubscribe(subscription.Id);
    }
}

The example stops after the first matching event only to remain finite.

Route notifications by subscription ID

The notification stream also carries relay status and other client-wide activity. For event notifications, compare the subscription ID with the ID returned by subscribe.

Subscription setup returns successful and failed relay outcomes. The pool restores active subscriptions after reconnection where possible, but this does not recover events missed while a relay was unavailable. Use a bounded catch-up read when that gap matters.

Close the subscription

Unsubscribing closes the matching relay-side state and prevents it from being restored after reconnection. If the client itself is dropped, its remaining subscriptions and connections stop with it.

The next chapter uses Negentropy sync to reconcile a durable local view after startup or a connection gap.