NDK
NDK is a Nostr development kit that makes the experience of building Nostr-related applications, whether they are relays, clients, or anything in between, better, more reliable and overall nicer to work with than existing solutions.
Besides improving the developer experience, the core goal of NDK is to improve the decentralization of Nostr via intelligent conventions and data discovery features without depending on any one central point of coordination (such as large relays or centralized search providers).
Installation
npm add @nostr-dev-kit/ndk
Support
NDK NIP-28 group chat
- note15m6rdfvlmd0z836hk83sg7r59xtv23qnmamhsslq5uc6744fdm4qfkeat3
Features
Instantiate an NDK instance
You can pass an object with several options to a newly created instance of NDK.
explicitRelayUrls
βΒ an array of relay URLs.signer
- an instance of a signer.cacheAdapter
- an instance of a Cache Adapterdebug
- boolean true/false to turn on degbugging
import NDK from "@nostr-dev-kit/ndk";
ndk = new NDK({ explicitRelayUrls: ["wss://a.relay", "wss://another.relay"] });
Note: In normal client use, it's best practice to instantiate NDK as a singleton class. See more below.
Connecting
After you've instatiated NDK, you need to tell it to connect before you'll be able to interact with any relays.
import NDK from "@nostr-dev-kit/ndk";
ndk = new NDK({ explicitRelayUrls: ["wss://a.relay", "wss://another.relay"] });
await ndk.connect();
Signers
NDK uses signers optionally passed in to sign events. Note that it is possible to use NDK without signing events (e.g. to get someone's profile).
Signing adapters can be passed in when NDK is instantiated or later during runtime.
Using a NIP-07 browser extension (e.g. Alby, nos2x)
Instatiate NDK with a NIP-07 signer
import NDK, { NDKNip07Signer, NDKEvent } from "@nostr-dev-kit/ndk";
const nip07signer = new NDKNip07Signer();
const ndk = new NDK({ signer: nip07signer });
NDK can now ask for permission, via their NIP-07 extension, to...
Read the user's public key
signer.user().then(async (user) => {
if (!!user.npub) {
console.log("Permission granted to read their public key.");
}
});
Sign & publish events
const ndkEvent = new NDKEvent(ndk);
ndkEvent.kind = 1;
ndkEvent.content = "Hello, world!";
ndkEvent.publish();
Caching
NDK provides database-agnostic caching functionality out-of-the-box to improve the performance of your application and reduce load on relays.
NDK will eventually allow you to use multiple caches simultaneously and allow for selective storage of data in the cache store that makes the most sense for your application.
Where to look is more important that long-term storage
The most important data to cache is where a user or note might be found. UX suffers profoundly when this type of data cannot be found. By design, the Nostr protocol leaves beadcrums of where a user or note might be found and NDK does it's best to store this data automatically and use it when you query for events.
Instantiating and using a cache adapter
const redisAdapter = new RedisAdapter(redisUrl);
const ndk = new NDK({ cacheAdapter: redisAdapter });
Buffered queries (COMING SOON)
Clients often need to load data (e.g. profile data) from individual components at once (e.g. initial page render). This typically causes multiple subscriptions to be submitted fetching the same information and causing poor performance or getting rate-limited/maxed out by relays.
NDK implements a convenient subscription model, buffered queries, where a named subscription will be created after a customizable amount of time, so that multiple components can append queries.
ndk.bufferedSubscription({ kinds: [0], authors: ["pubkey-1"] }, "profiles", 500);
ndk.bufferedSubscription({ kinds: [0], authors: ["pubkey-2"] }, "profiles", 500);
In this example, NDK will wait 500ms before creating a subscription with the filter:
{kinds: [0], authors: ['pubkey-1', 'pubkey-2'] }
Intelligent relay selection
When a client submits a request through NDK, NDK will calculate which relays are most likely able to satisfy this request.
Queries submitted by the client might be broken into different queries if NDK computes different relays.
For example, say npub-A follows npub-B and npub-C. If the NDK client uses:
const ndk = new NDK({ explicitRelays: ["wss://nos.lol"] });
const npubA = ndk.getUser("npub-A");
const feedEvents = await npubA.feed();
This would result in the following request:
{ "kinds": [1], "authors": ["npub-B", "npub-C"] }
But if NDK has observed that npub-B
tends to write to wss://userb.xyz
and
npub-C
tends to write to wss://userc.io
, NDK will instead send the following queries.
{ "kinds": [1], "authors": [ "npub-B", "npub-C" ] }
{ "kinds": [1], "authors": [ "npub-B" ] }
{ "kinds": [1], "authors": [ "npub-C" ] }
Auto-closing subscriptions
Often, clients need to fetch data but don't need to maintain an open connection to the relay. This is true of profile metadata requests especially.
- The
autoclose
flag will make the connection close immediately after EOSE is seen. - An integer
autoclose
will close the connection after that amount of ms after EOSE is seen.
ndk.subscription({ kinds: [0], authors: ["..."] }, { autoclose: true });
Convenience methods
NDK implements several conveience methods for common queries.
Instantiate a user by npub or hex pubkey
This is a handy method for instantiating a new NDKUser
and associating the current NDK instance with that user for future calls.
const pablo = ndk.getUser({
npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft"
});
const jeff = ndk.getUser({
hexpubkey: "1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef"
});
Fetch a user's profile and publish updates
You can easily fetch a user's profile data from kind: 0
events on relays. Calling .fetchProfile()
will update the profile
attribute on the user object instead of returning the profile directly. NDK then makes it trivial to update values and publish those updates back to relays.
const pablo = ndk.getUser({
npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft"
});
await pablo.fetchProfile();
const pabloFullProfile = pablo.profile;
pablo.profile.name = "Pablo";
await pablo.publish();
Finding a single event or all events matching a filter
You can fetch the first event or all events that match a given set of filters.
const filter: NDKFilter = { kinds: [1], authors: [hexpubkey1, hexpubkey2] };
event = await ndk.fetchEvent(filter);
events = await ndk.fetchEvents(filter);
Creating & publishing events
const ndk = new NDK({ explicitRelays, signer });
const event = new NDKEvent(ndk);
event.kind = 1;
event.content = "PV Nostr! π€πΌ";
await ndk.publish(event);
Reacting to an event
const event = await ndk.fetchEvent({ author: "jack@cashapp.com" })[0];
await event.react("π€");
Zap an event
const event = await ndk.fetchEvent({ author: "jack@cashapp.com" })[0];
await event.zap(1337, "Zapping your post!");
Architecture decisions & suggestions
- Users of NDK should instantiate a single NDK instance.
- That instance tracks state with all relays connected, explicit and otherwise.
- All relays are tracked in a single pool that handles connection errors/reconnection logic.
- RelaySets are assembled ad-hoc as needed depending on the queries set, although some RelaySets might be long-lasting, like the
explicitRelays
specified by the user. - RelaySets are always a subset of the pool of all available relays.