NIP-09

Event Deletion

final content management

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.

Author
fiatjaf
Last Updated
15 January 2024
Official Spec
View on GitHub →

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

  1. Mistakes & Typos: Delete a post with errors and repost corrected version
  2. Privacy Concerns: Remove content with accidentally shared sensitive info
  3. Changed Mind: Retract statements or opinions
  4. Spam Control: Remove spam or unwanted test posts
  5. 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 5 for deletion events
  • tags: One or more e tags referencing events to delete
  • content: Optional explanation (human-readable reason)
  • pubkey: Must match the author of events being deleted

Key Rules

  1. Own events only: You can only delete your own events (same pubkey)
  2. Multiple deletions: One kind 5 event can delete multiple events
  3. Deletion events persist: The kind 5 event itself remains on relays
  4. 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 pubkey matches 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

  1. User clicks “Delete” on their post
  2. Client confirms with dialog (“Are you sure?”)
  3. Client creates kind 5 event with post’s event ID
  4. Client publishes to all relays user is connected to
  5. Client immediately hides the post from UI (optimistic update)
  6. 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 pubkey must match original event pubkey
  • 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:

  1. Archival Services: May intentionally preserve all events
  2. Client Caches: Users who fetched the event still have it
  3. Screenshots: Users may have captured content
  4. Non-Compliant Relays: May ignore deletion requests
  5. 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

  1. Think before posting: Deletion is not guaranteed
  2. Delete promptly: Sooner = fewer people cached it
  3. Provide reason: Optional content field helps context
  4. Verify deletion: Check if post disappears from your clients
  5. Don’t rely on deletion for sensitive content

For Developers (Clients)

  1. Make deletion easy: Clear UI for deleting posts
  2. Confirm deletions: “Are you sure?” dialog
  3. Publish to all relays: Increase deletion success rate
  4. Optimistic updates: Hide post immediately in UI
  5. Check deletion events: Filter out deleted content
  6. Cache deletion events: Avoid repeated fetches

For Developers (Relays)

  1. Verify author: Ensure deletion request matches original author
  2. Process promptly: Delete as soon as deletion event received
  3. Document policy: Make deletion policy clear
  4. Consider exceptions: Archival vs. ephemeral content policies
  5. 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);

  • 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:


Last updated: January 2024 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

damus primal amethyst snort iris coracle nostrudel satellite nos nostur plebstr
View all clients →

Related NIPs

NIP-01 NIP-16 NIP-40
← Browse All NIPs