As you begin using many of the twitter-like clients built on Nostr, you may notice that delete post functionality is missing from many of them. The reason for this is due to the decentralized nature of Nostr, since a client can connect to multiple relays, it can take some time for a deletion to propagate across relays; moreover relay connections may be lost. Since there is no requirement that a relay has to adhere to the protocol spec, a relay may not even respect a deletion event if it decides not to. Likewise, individual clients can decide whether or not to implement delete functionality, just as with any other NIP. Therefore, it is more accurate to describe the event as a deletion request where it is the responsibility of the individual clients to implement this event (or in many cases, not) as they choose. The NIP in question is NIP-09 which has the tags draft and optional. Nevertheless, this post will go through some considerations and then run through a simple implementation of this functionality if you have chosen to implement it on your Nostr client.

Considerations
Before we start implementing, we should go over some key considerations for the deletion functionality.
Probably the most important is understanding that clients do not have to implement deletion functionality at all; they have no responsibility to respect delete events. This means that some clients may not show deleted notes while others will continue to show them. We must be comfortable with the fact that publishing notes to Nostr relay(s) can be considered permanent record.
Deletion events are associated with the original event and can be retrieved from relays by filtering on the event kind 5.
Due to this delete implementation, and not having any strict specification on who can or can’t make a delete request for a note, we can decide how to implement this functionality within our own client. Different clients could have different logic here, where some may say that only the original poster of the note can send a delete request, while others could have custom permissions based logic so that certain types of users can delete any notes from any users (say, an admin user), and so on.
Implementation
If we are comfortable with all of these considerations, we can go ahead and implement some simple handling of deletion functionality on our client.
First, create a helper function that will publish the delete event:
//NIP-09: https://github.com/nostr-protocol/nips/blob/master/09.md
export async function deletePost(id: string, pool: SimplePool | null, nostrExists: boolean) {
if (nostrExists) {
const event = {
kind: 5,
created_at: Math.floor(Date.now() / 1000),
content: "Post deleted",
tags: [
['e', id],
['k', '1']
],
};
try {
const signedEvent = await window.nostr.signEvent(event);
await pool?.publish(RELAYS, signedEvent);
return {
success: true,
};
} catch {
console.log("Unable to delete post");
}
}
return {
success: false,
}
}
We will go ahead and add an extra check in our application for any deletion events for posts retrieved, and only add this post to our list for display if no deletion events were found for it:
useEffect(() => {
if (!props.pool) return;
setLoading(true);
setEvents([]);
const subPosts = props.pool.subscribeMany(RELAYS, [{
kinds: [1],
limit: 50,
}],
{onevent(event: ExtendedEvent) {
props?.pool?.subscribeMany(
RELAYS,
[{
kinds: [5],
'#e': [event.id],
}],
{
onevent(deleteEvent) {
if (deleteEvent.pubkey === event.pubkey) event.deleted = true;
},
oneose() {
if (!event.deleted) {
setEvents((events) => insertEventIntoDescendingList(events, event));
}
}
}
);
}});
return () => {
subPosts.close();
}
}, [props.pool]);
Update our presentation to show the delete functionality, and (in our case) only allow the original author to delete their own post:
const [canDelete, setCanDelete] = useState(false);
...
useEffect(() => {
if (keyValue) {
try {
let skDecoded = bech32Decoder('nsec', keyValue);
let pk = getPublicKey(skDecoded);
if (pk === user.pubkey) {
setCanDelete(true);
}
}
catch {}
}
}, [keyValue]);
...
const handleDelete = (id: string) => {
deletePost(id, pool, nostrExists).then((result) => {
if (result.success) {
toast.success("Post deleted");
setDeleted(true);
}
else {
toast.error("Failed to delete post");
}
});
}
if (deleted) {
return (
<div className="rounded p-16 border border-gray-600 bg-gray-700 flex flex-col gap-16 break-words">
<p className="text-body3 text-gray-400">This post has been deleted</p>
</div>
);
}
...
<div className="p-4 pl-32">
<TrashIcon
className={canDelete ? "h-6 w-6 text-blue-500 cursor-pointer" : "h-6 w-6 text-grey-500 cursor-not-allowed"}
title={canDelete ? "Delete this post" : "You cannot delete this post"}
onClick={canDelete ?() => handleDelete(id) : undefined}
/>
</div>
We have now implemented delete functionality on our Nostr client!


Further Reading
- Part 1: A Gentle Introduction to Nostr
- Part 2: One Object To Rule Them All!
- Part 3: Setting Up A Relay
- Part 4: My First Client
- Part 5: Zapped By Lightning!
- Part 6: Reactions
- NIP-09 Specification
- NIP-09/Deletion Discussion on GitHub
Like this post and want to support the series? Tip me some sats on lightning at mikkthemagnificent@getalby.com:
