Event Deletion
NIP-09 allows users to request deletion of their events by publishing kind 5 deletion events. Relays may honor these requests, but deletion is not guaranteed due to Nostr's decentralized nature.
NIP-09: Event Deletion
Status: Final Author: fiatjaf Category: Content Management
Overview
NIP-09 defines how Nostr users can request deletion of their previously published events. This is done by publishing a special kind 5 event that indicates which events should be deleted.
Key Points:
- ✅ Users can request deletion of their own events
- ⚠️ Deletion is a request, not a guarantee
- ⚠️ Relays may or may not honor deletion requests
- ⚠️ Events already fetched by clients remain cached
- ⚠️ No way to force deletion across the network
Important: Nostr’s decentralized architecture means true deletion is impossible. Once an event is published, it may be stored on multiple relays and in multiple caches. NIP-09 provides a best-effort mechanism for content removal.
Why Event Deletion?
Use Cases
- Mistakes & Typos: Delete a post with errors and repost corrected version
- Privacy Concerns: Remove content with accidentally shared sensitive info
- Changed Mind: Retract statements or opinions
- Spam Control: Remove spam or unwanted test posts
- Account Cleanup: Delete old content when closing accounts
Limitations
What NIP-09 CAN do:
- Request relays to delete events
- Signal intent to delete to other clients
- Provide user interface for “deleting” posts
What NIP-09 CANNOT do:
- Force deletion from all relays
- Delete events from users who already cached them
- Prevent archival services from storing events
- Guarantee complete removal from the network
How It Works
Deletion Event Structure
To request deletion, publish a kind 5 event:
{
"kind": 5,
"created_at": 1673347337,
"content": "These posts were published by mistake",
"tags": [
["e", "event_id_to_delete_1"],
["e", "event_id_to_delete_2"],
["e", "event_id_to_delete_3"]
],
"pubkey": "your_public_key",
"id": "...",
"sig": "..."
}
Field Breakdown:
- kind: Always
5for deletion events - tags: One or more
etags referencing events to delete - content: Optional explanation (human-readable reason)
- pubkey: Must match the author of events being deleted
Key Rules
- Own events only: You can only delete your own events (same pubkey)
- Multiple deletions: One kind 5 event can delete multiple events
- Deletion events persist: The kind 5 event itself remains on relays
- Relay discretion: Each relay decides whether to honor deletions
Creating a Deletion Request
Example: Delete a Single Event
import { getPublicKey, signEvent } from 'nostr-tools';
// Event ID you want to delete
const eventIdToDelete = "4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65";
// Create deletion event
const deletionEvent = {
kind: 5,
created_at: Math.floor(Date.now() / 1000),
tags: [
["e", eventIdToDelete]
],
content: "Deleting this post - it contained an error",
pubkey: getPublicKey(privateKey)
};
// Sign and publish
const signedEvent = signEvent(deletionEvent, privateKey);
relay.publish(signedEvent);
Example: Delete Multiple Events
const eventsToDelete = [
"event_id_1",
"event_id_2",
"event_id_3"
];
const deletionEvent = {
kind: 5,
created_at: Math.floor(Date.now() / 1000),
tags: eventsToDelete.map(id => ["e", id]),
content: "Cleaning up test posts",
pubkey: getPublicKey(privateKey)
};
const signedEvent = signEvent(deletionEvent, privateKey);
relay.publish(signedEvent);
Example: Delete All Events of a Specific Kind
// Use 'k' tag to specify event kind
const deletionEvent = {
kind: 5,
created_at: Math.floor(Date.now() / 1000),
tags: [
["k", "1"] // Delete all kind 1 (text notes)
],
content: "Deleting all my text notes",
pubkey: getPublicKey(privateKey)
};
Note: Using k tags to delete by kind is optional and not all relays support it.
Relay Behavior
Relay Decision-Making
Relays have full discretion on whether to honor deletion requests:
Compliant Relays:
- Check deletion event signature
- Verify
pubkeymatches original event author - Remove events from storage
- Stop serving deleted events to new requests
Non-Compliant Relays:
- May ignore deletion requests entirely
- May store deletion events but not act on them
- May require payment to process deletions
- May have policies against deletion (archival relays)
Checking Deletion Status
Clients should check for deletion events when fetching content:
// 1. Fetch events
const events = await relay.get({
kinds: [1],
authors: [pubkey],
limit: 100
});
// 2. Fetch deletion events for this author
const deletions = await relay.get({
kinds: [5],
authors: [pubkey]
});
// 3. Extract deleted event IDs
const deletedIds = new Set();
deletions.forEach(deletion => {
deletion.tags.forEach(tag => {
if (tag[0] === "e") {
deletedIds.add(tag[1]);
}
});
});
// 4. Filter out deleted events
const activeEvents = events.filter(event => !deletedIds.has(event.id));
Client Behavior
Displaying Deletions
Clients should handle deletion events gracefully:
Option 1: Hide Deleted Events
// Don't display events that have deletion requests
if (deletedIds.has(event.id)) {
return null; // Skip rendering
}
Option 2: Show Placeholder
// Show "This post was deleted" message
if (deletedIds.has(event.id)) {
return <div className="deleted">This post was deleted by the author</div>;
}
Option 3: Strike Through
// Show content but struck through
if (deletedIds.has(event.id)) {
return <div className="deleted-content"><s>{event.content}</s></div>;
}
Delete UI Workflow
- User clicks “Delete” on their post
- Client confirms with dialog (“Are you sure?”)
- Client creates kind 5 event with post’s event ID
- Client publishes to all relays user is connected to
- Client immediately hides the post from UI (optimistic update)
- Client may show confirmation (“Post deleted successfully”)
Security Considerations
Only Author Can Delete
Relays must verify that the deletion request comes from the original author:
// Relay-side validation
function isValidDeletion(deletionEvent, originalEvent) {
// Check signature is valid
if (!verifySignature(deletionEvent)) {
return false;
}
// Check deletion author matches original author
if (deletionEvent.pubkey !== originalEvent.pubkey) {
return false;
}
return true;
}
Deletion Cannot Be Spoofed
An attacker cannot delete someone else’s events:
- Deletion event
pubkeymust match original eventpubkey - Signature ensures only the private key holder can create deletions
- Relays reject deletion requests from wrong pubkey
Advanced Topics
Partial Deletion
Some content types support partial deletion:
Thread Deletion:
// Delete entire thread (root + all replies)
const deletionEvent = {
kind: 5,
tags: [
["e", rootEventId],
["e", replyEventId1],
["e", replyEventId2]
],
content: "Deleting this conversation"
};
Profile Deletion:
// Delete profile (kind 0)
const deletionEvent = {
kind: 5,
tags: [
["e", profileEventId]
],
content: "Removing my profile"
};
Deletion of Replaceable Events
For replaceable events (kind 0, 3, 10000-19999):
- Publishing a new event already “replaces” the old one
- Deletion may be redundant
- Use empty/minimal content to “clear” instead
Example: Clear Profile:
{
"kind": 0,
"content": "{}", // Empty profile
"tags": [],
...
}
Deletion Event Lifespan
Deletion events persist forever:
- They are regular events stored on relays
- They accumulate over time
- Clients must always check for them
Optimization: Clients can cache deletion events to avoid repeated fetches.
Privacy Implications
Deletion Doesn’t Mean Privacy
⚠️ Critical Understanding: Deleted events may still exist:
- Archival Services: May intentionally preserve all events
- Client Caches: Users who fetched the event still have it
- Screenshots: Users may have captured content
- Non-Compliant Relays: May ignore deletion requests
- Relay Backups: May be restored from backups
Best Practice: Don’t post what you can’t afford to be public forever.
Use Cases for Deletion
Appropriate:
- Correcting mistakes (then reposting correct version)
- Removing spam or test posts
- Cleaning up old content
- Managing account appearance
Not Appropriate:
- Hiding evidence of wrongdoing (won’t work)
- Privacy-sensitive information (already exposed)
- Legal content removal (not enforceable)
Client Support
Full Support (Delete Button)
- Damus - Long-press to delete posts
- Primal - Delete option in post menu
- Amethyst - Comprehensive deletion with confirmation
- Snort - Delete with optional reason
- Iris - Delete from post options
- Nostrudel - Advanced deletion tools
Relay Compliance
- Most public relays: Honor deletion requests
- Some relays: May charge for deletions (anti-spam)
- Archival relays: Explicitly do not delete (preservationist policy)
Check our Client Directory and Relay Directory for specifics.
Best Practices
For Users
- Think before posting: Deletion is not guaranteed
- Delete promptly: Sooner = fewer people cached it
- Provide reason: Optional
contentfield helps context - Verify deletion: Check if post disappears from your clients
- Don’t rely on deletion for sensitive content
For Developers (Clients)
- Make deletion easy: Clear UI for deleting posts
- Confirm deletions: “Are you sure?” dialog
- Publish to all relays: Increase deletion success rate
- Optimistic updates: Hide post immediately in UI
- Check deletion events: Filter out deleted content
- Cache deletion events: Avoid repeated fetches
For Developers (Relays)
- Verify author: Ensure deletion request matches original author
- Process promptly: Delete as soon as deletion event received
- Document policy: Make deletion policy clear
- Consider exceptions: Archival vs. ephemeral content policies
- Log deletions: For debugging and transparency
Common Questions
Can I delete someone else’s post?
No. You can only delete your own events. Relays verify the deletion request comes from the original author.
What if a relay ignores my deletion request?
You have no recourse. Relay operators have full autonomy. Consider using different relays or accepting that some copies may persist.
Can I “undo” a deletion?
No. Once deleted, you’d need to repost the content. The original event ID is lost.
Do clients show deleted posts?
Depends on the client:
- Some hide them completely
- Some show “[deleted]” placeholder
- Some strike through the content
Can I delete my entire account?
You can delete all your events, but:
- Your public key still exists
- Deletion events remain on relays
- Cached copies may persist
- True “account deletion” doesn’t exist
Implementation Example
Complete Deletion Workflow
import { getPublicKey, signEvent } from 'nostr-tools';
class DeletionManager {
constructor(relays, privateKey) {
this.relays = relays;
this.privateKey = privateKey;
this.pubkey = getPublicKey(privateKey);
}
// Delete one or more events
async deleteEvents(eventIds, reason = "") {
const deletionEvent = {
kind: 5,
created_at: Math.floor(Date.now() / 1000),
tags: eventIds.map(id => ["e", id]),
content: reason,
pubkey: this.pubkey
};
const signedEvent = signEvent(deletionEvent, this.privateKey);
// Publish to all relays
const promises = this.relays.map(relay =>
relay.publish(signedEvent)
);
await Promise.all(promises);
return deletionEvent.id;
}
// Get all deletion events for this user
async getDeletionEvents() {
const filters = {
kinds: [5],
authors: [this.pubkey]
};
const deletions = await Promise.all(
this.relays.map(relay => relay.get(filters))
);
return deletions.flat();
}
// Get set of deleted event IDs
async getDeletedEventIds() {
const deletions = await this.getDeletionEvents();
const deletedIds = new Set();
deletions.forEach(deletion => {
deletion.tags.forEach(tag => {
if (tag[0] === "e") {
deletedIds.add(tag[1]);
}
});
});
return deletedIds;
}
// Filter events, removing deleted ones
async filterDeletedEvents(events) {
const deletedIds = await this.getDeletedEventIds();
return events.filter(event => !deletedIds.has(event.id));
}
}
// Usage
const manager = new DeletionManager(relays, privateKey);
// Delete a post
await manager.deleteEvents(
["event_id_1", "event_id_2"],
"Deleting test posts"
);
// Get list of deleted event IDs
const deleted = await manager.getDeletedEventIds();
// Filter events to remove deleted ones
const cleanEvents = await manager.filterDeletedEvents(allEvents);
Related NIPs
- NIP-01 - Basic protocol (event structure)
- NIP-16 - Event treatment (how to handle different event kinds)
- NIP-40 - Expiration timestamp (auto-deletion alternative)
Technical Specification
For the complete technical specification, see NIP-09 on GitHub.
Summary
NIP-09 provides a deletion mechanism for Nostr:
✅ Kind 5 events request deletion ✅ E tags specify which events to delete ✅ Author verification prevents unauthorized deletions ✅ Relay discretion - each relay decides compliance
⚠️ Limitations:
- Deletion is a request, not guaranteed
- Events may persist on some relays
- Client caches retain deleted content
- True deletion is impossible
Best practice: Don’t post sensitive information. Deletion is best-effort, not guaranteed.
Next Steps:
- Learn about replies in NIP-10
- Explore reactions in NIP-25
- Understand expiration in NIP-40
- Browse all NIPs in our reference
Last updated: January 2024 Official specification: GitHub
Client Support
This NIP is supported by the following clients: