Skip to content

Subscribing to Events

Once connected, you can subscribe to events using ndk.subscribe() by providing filters you can specify the events you're interested in.

More about how this all works in the dedicated section about subscription internals.

Subscribe

The ndk.subscribe() method accepts these parameters:

  • filters: A single or array of NDKFilter. See NIP-01.
  • opts?: Subscription options object NDKSubscriptionOptions.
  • autoStart?: Event handlers for that subscription.
ts
import NDK from "@nostr-dev-kit/ndk";

const ndk = new NDK();

ndk.subscribe(
    { kinds: [1] }, // Filters
    { closeOnEose: true }, // Options (no explicit relays specified)
);

Specifying Relays

By default, NDK will use the already connected relay set or provided through the signer. You can override this behavior by providing explicit relays in the relayUrls or relaySet options.

ts
import NDK, {NDKRelaySet} from "@nostr-dev-kit/ndk";

const ndk = new NDK();
const explicitRelaySet = NDKRelaySet.fromRelayUrls(["wss://explicit.relay"], ndk);
ndk.subscribe(
    {kinds: [7]}, // Filters
    {
        // Options object now includes relaySet
        closeOnEose: true,
        relaySet: explicitRelaySet,
    },
);

By default, NDK subscriptions use cross-subscription matching: when an event comes in from any relay, it's delivered to all subscriptions whose filters match, regardless of which relays the subscription was targeting.

Event Handlers

Handler Functions

TIP

The recommended way to handle events is to provide handler functions directly when calling ndk.subscribe(). This is done using the third argument (autoStart), which accepts an object containing onEvent, onEvents, and/or onEose callbacks.

Why is this preferred? Subscriptions can start receiving events (especially from a fast cache) almost immediately after ndk.subscribe() is called. By providing handlers directly, you ensure they are attached before any events are emitted, preventing potential race conditions where you might miss the first few events if you attached handlers later using .on().

ts
import NDK, { type NDKEvent, type NDKRelay, type NDKSubscription } from "@nostr-dev-kit/ndk";

const ndk = new NDK();

ndk.subscribe(
    { kinds: [1] }, // Filters
    { closeOnEose: true }, // Options (no explicit relays specified)
    {
        // Direct handlers via autoStart parameter (now the 3rd argument)
        onEvent: (event: NDKEvent, relay?: NDKRelay) => {
            // Called for events received from relays after the initial cache load (if onEvents is used)
            console.log("Received event from relay (id):", event.id);
        },
        onEvents: (events: NDKEvent[]) => {
            // Parameter renamed to 'events'
            console.log(`Received ${events.length} events from cache initially.`);
        },
        onEose: (subscription: NDKSubscription) => {
            console.log("Subscription reached EOSE:", subscription.internalId);
        },
    },
);

Attaching Handlers

You can also attach event listeners after creating the subscription using the .on() method.

WARNING

While functional, be mindful of the potential race condition mentioned above, especially if you rely on immediate cache results.

ts
import NDK from "@nostr-dev-kit/ndk";

const ndk = new NDK();

const subscription = ndk.subscribe(
    { kinds: [1] }, // Filters
    { closeOnEose: true }, // Options (no explicit relays specified)
);

// Attach handlers later
subscription.on("event", (event) => {
    console.log("Received event:", event.id);
});
subscription.on("eose", () => {
    console.log("Initial events loaded");
});

// Remember to stop the subscription when it's no longer needed
// setTimeout(() => subscription.stop(), 5000);

Functions

onEvent

The onEvent handler is called for every event received from relays or the cache.

onEvents

Using the onEvents handler provides an efficient way to process events loaded from the cache. When you provide onEvents:

  1. If NDK finds matching events in its cache synchronously when the subscription starts, onEvents is called once with an array of all those cached events.
  2. The onEvent handler is skipped for this initial batch of cached events.
  3. onEvent will still be called for any subsequent events received from relays or later asynchronous cache updates.

This is ideal for scenarios like populating initial UI state, as it allows you to process the cached data in a single batch, preventing potentially numerous individual updates that would occur if onEvent were called for each cached item.

If you don't provide onEvents, the standard onEvent handler will be triggered for every event, whether it comes from the cache or a relay.

onEose

Called when the subscription is closed.

Targetting Relays

By default, NDK subscriptions use cross-subscription matching: when an event comes in from any relay, it's delivered to all subscriptions whose filters match, regardless of which relays the subscription was targeting.

The exclusiveRelay option allows you to create subscriptions that only accept events from their specified relays, ignoring events that match the filter but come from other relays.

ts
import NDK from "@nostr-dev-kit/ndk";

const ndk = new NDK();

// Subscription that ONLY accepts events from relay-a.com
const exclusiveSub = ndk.subscribe(
    {kinds: [7]},
    {
        relayUrls: ["wss://relay-a.com"],
        exclusiveRelay: true, // 🔑 Key option
    },
);

exclusiveSub.on("event", (event) => {
    console.log("Event from relay-a.com:", event.content);
    // This will ONLY fire for events from relay-a.com
    // Events from relay-b.com or relay-c.com are rejected
});

Without exclusiveRelay, subscriptions receive events from any relay (Cross-Subscription Matching).

More information, use-cases and examples of exclusive relays is available in the advanced exclusive relay documentation.

Code Snippets

More snippets and examples can be found in the snippets directory