Nostr Series — Part 4: My First Client

Michael Stewart
4 min readApr 22, 2024

--

Now that we have set up a relay, let’s start using it by creating our first Nostr client. All Nostr clients connect to one or more relays, to publish and subscribe to events.

A popular application for a Nostr client is a messaging app where users publish, read and respond to posts, similar to Twitter. There could be many other different applications of the Nostr protocol, such as instant messaging, streaming and gaming. In March 2023, Jack Dorsey put out a bounty for 10 BTC to anyone that creates GitHub on Nostr 😲

Getting Started

There are no restrictions on what technologies to use when developing a Nostr application. React, Angular and Vue are popular javascript-based frameworks for front-end web applications. A developer may choose a framework based on personal preference, or select one that has some already well established libraries that work with Nostr.

Libraries

  • nostr-tools — a handy package providing functionality for working with Nostr, such as connecting to relays and signing events
  • bech32 — library for encoding and decoding using the bech32 standard, used for keys in both Bitcoin and Nostr
  • getalby/lightning-tools — handle Bitcoin Lightning payments

Key Management

Your private key is your identity and should never be exposed by any client. As a developer building on the Nostr protocol, we should be responsible to eliminate any possibility of private keys being leaked. To this end, we can take advantage of the window.nostr object that is populated by browser extensions such as Alby and nos2x. These extensions ensure that you never have to give your private key to any web application when signing events. Hardware signing devices such as LNBits Nostr Signing Device (NSD) are excellent devices that may also be used to add an extra layer of security.

Subscriptions

We will begin by connecting to the relay set up in Part 3. This will create a WebSocket connection that will persist for the duration of the user’s session.

Connecting to the relay:

export const RELAYS = [
"wss://relay.ghostcopywrite.com"
]
import { SimplePool } from "nostr-tools";
...
const [pool, setPool] = useState<SimplePool | null>(null);
...
useEffect(() => {
const _pool = new SimplePool();
setPool(_pool);

return () => {
_pool.close(RELAYS);
}
}, []);

Subscribing to all events of kind 1 (short text message):

 const subPosts = pool.subscribeMany(RELAYS, [{
kinds: [1],
limit: 5,
//"#t": ["bitcoin"] - example filter
}],
{onevent(event) {
//do something with the post/event
}});
...
//don't forget to clean up and close the subscription when done
return () => {
subPosts.close();
}

We can also subscribe to events with kind 0, for retrieving metadata (ie. user data) given the user’s pubkey returned in the event.

Publishing Events

Events are signed with the user’s private key to generate a signature, and then sent to one or more relays.

    async function sendMessage() {
if (!pool) return;
if (window.nostr) {
let event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: message,
}
await window.nostr.signEvent(event).then(async (eventToSend: any) => {
await pool?.publish(RELAYS, eventToSend);
});
}
else {
//keyValue is a user input
let skDecoded = bech32Decoder('nsec', keyValue);
let pk = getPublicKey(skDecoded);
let event = {
kind: 1,
pubkey: pk,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: message,
}
let eventFinal = finalizeEvent(event, skDecoded);
await pool?.publish(RELAYS, eventFinal);
}
}
  • If window.nostr is detected (ie. user has one of the aforementioned browser extensions for key management), we access the signEvent function to sign the event before publishing it to our specified relays
  • If window.nostr is not detected, we use the bech32 library to decode the key which is used to sign the event before publishing it to the relays

The bech32 decoder function:

export function bech32Decoder(currPrefix: string, data: string) {
const { prefix, words } = bech32.decode(data);
if (prefix !== currPrefix) {
throw Error('Invalid address format');
}
return Buffer.from(bech32.fromWords(words));
}

Conclusion

Hopefully, this post has demonstrated how developer-friendly working with Nostr really is. Development at the protocol level is continuously evolving, adding functionality and expanding the wide range of possible applications that can be built on top of the protocol. Tools and libraries are being created to further enhance the development experience, enabling developers to build decentralized, censorship-resistant communication applications that can empower individuals everywhere.

Like this post and want to support the series? Tip me some sats on lightning at mikkthemagnificent@getalby.com:

--

--