Note
This XIP replaces XIP-61: Edit and delete messages.
Abstract
This proposal specifies a best-effort mechanism for deleting messages in XMTP group conversations. It enables original message senders and super admins to request deletion of messages, with proper authorization checks and query-time message filtering to present deleted messages to users.
Motivation
Users need the ability to delete messages they accidentally sent or to remove harmful, inappropriate, or policy-violating content from group conversations. In group chat moderation scenarios, super admins require the authority to delete any memberâs message to maintain community standards. This feature enhances user experience and provides essential moderation capabilities for group administrators.
Specification
Delete message content type
A new app-level content type DeleteMessage will be introduced:
message DeleteMessage {
// The ID of the message to be deleted
string message_id = 1;
}
Content type identifier:
- Authority ID:
xmtp.org - Type ID:
deleteMessage - Version:
1.0
Authorization rules
A DeleteMessage can be sent by:
- Original sender: The inbox that sent the original message
- Super admin: Any super admin of the group chat
Any other inbox attempting to send a DeleteMessage MUST receive an authorization error.
Message deletion workflow
1. Send a delete message
When a user or super admin initiates a delete:
// Validates authorization and sends delete message
// Returns error if:
// - message_id doesn't exist in the group (MessageNotFound)
// - sender is not the original message sender AND not a super admin (NotAuthorizedToDelete)
// - message is a transcript/membership change message (CannotDeleteTranscriptMessage)
conversation.delete_message(message_id).await?;
The SDK:
- Validates the message exists in the group
- Checks authorization (original sender OR super admin)
- Creates and sends a
DeleteMessagewith the targetmessage_id - Locally marks the message as deleted in the database
2. Receive a delete message
When a client receives a DeleteMessage:
-
Attempt to locate the original message by
message_id- If found: Proceed with validation
- If NOT found: Still store the deletion (see âHandle out-of-order deliveryâ below)
-
Validate the delete request:
- If original message exists AND senderâs
inbox_idmatches original messageâssender_inbox_id: Mark as deleted by sender - Else if sender is a super admin at the time of deletion: Mark as deleted by super admin
- Otherwise: Ignore the delete message (invalid authorization)
- If original message exists AND senderâs
-
Store the deletion in
message_deletionstable:- The
DeleteMessageitself is stored ingroup_messages(like any other message) - Create a record in
message_deletions:id: Themessage_idof theDeleteMessageitselfdeleted_message_id: The target message to be deleteddeleted_by_inbox_id: Who sent theDeleteMessageis_super_admin_deletion: Whether they were a super admindeleted_at_ns: When the deletion was processed
- The
-
Handle out-of-order delivery:
- If the original message hasnât arrived yet, the deletion record is still stored
- When the original message eventually arrives, queries will automatically filter it
- This prevents race conditions in decentralized message delivery
3. Database schema
Add a new table to track deletions:
-- Deletions table
-- The id is the message_id of the DeleteMessage itself (from group_messages table)
-- The deleted_message_id references the original message being deleted
CREATE TABLE message_deletions (
-- Primary key: the ID of the DeleteMessage in the group_messages table
id BLOB PRIMARY KEY,
-- Group this deletion belongs to
group_id BLOB NOT NULL,
-- The ID of the original message being deleted
deleted_message_id BLOB NOT NULL,
-- The inbox_id of who sent the delete message
deleted_by_inbox_id TEXT NOT NULL,
-- Whether the deleter was a super admin at deletion time
is_super_admin_deletion BOOLEAN NOT NULL,
-- Timestamp when the deletion was processed
deleted_at_ns BIGINT NOT NULL,
-- Foreign key to the DeleteMessage in group_messages
FOREIGN KEY (id) REFERENCES group_messages(id),
-- Note: We don't use a foreign key for deleted_message_id because:
-- In a decentralized network, the DeleteMessage might arrive before the original message.
-- This allows us to store the deletion intent even if the target message hasn't arrived yet.
UNIQUE(deleted_message_id, id)
);
CREATE INDEX idx_message_deletions_deleted_message_id ON message_deletions(deleted_message_id);
CREATE INDEX idx_message_deletions_group_id ON message_deletions(group_id);
Handle out-of-order message delivery
Since XMTP is a decentralized network, messages may arrive in different order. The implementation MUST handle these scenarios:
-
Delete arrives before original message:
- Store the deletion in
message_deletionstable with thedeleted_message_id - When the original message arrives later:
- Check if a deletion exists for its
message_id - If found, immediately mark it as deleted in queries
- The original message is stored normally in
group_messagesbut will be filtered at query time
- Check if a deletion exists for its
- Store the deletion in
-
Original message arrives before delete:
- Standard flow: original message exists, deletion is applied when DeleteMessage arrives
-
Multiple deletes for the same message:
- The
UNIQUE(deleted_message_id, id)constraint allows multiple delete attempts - Only the first valid deletion (by sender or super admin) should be honored
- Subsequent deletes from unauthorized users are ignored
- The
Query-time filtering
Message queries
When querying messages (e.g., conversation.messages()), the SDK MUST:
- Check if each message has an associated valid deletion
- Replace deleted messages with a placeholder:
- If
deleted_by_inbox_idmatchessender_inbox_id: ReturnDeletedMessage(deletedBy: "sender") - If
deleted_by_super_adminis true: ReturnDeletedMessage(deletedBy: "superAdmin")
- If
- NEVER return the
DeleteMessageitself in message lists - NEVER return the original message content if deleted
Example message structure:
// Extended MessageBody enum to include DeletedMessage variant
#[derive(Debug, Clone)]
pub enum MessageBody {
Text(Text),
Reply(Reply),
Reaction(ReactionV2),
Attachment(Attachment),
RemoteAttachment(RemoteAttachment),
MultiRemoteAttachment(MultiRemoteAttachment),
TransactionReference(TransactionReference),
GroupUpdated(GroupUpdated),
ReadReceipt(ReadReceipt),
WalletSendCalls(WalletSendCalls),
Custom(EncodedContent),
// New variant for deleted messages
DeletedMessage { deleted_by: DeletedBy },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeletedBy {
Sender,
Admin(String), // inbox_id of the super admin who deleted the message
}
Conversation list
The ConversationListItem MUST handle deleted messages:
- If the last message is deleted:
- Show placeholder:
DeletedMessage(deletedBy: ...) - Preserve the message timestamp and count
- Show placeholder:
- Find the most recent non-deleted message to display
Pagination considerations
When paginating messages, the SDK MUST:
- Load deletion records for the entire group (or a sufficiently large window)
- Apply deletion filtering to ALL messages in the paginated result
- Ensure deletions that occurred after the pagination window are still applied
- This may require loading deletion metadata separately from message content
Restrictions
Non-deletable messages
The following message types CANNOT be deleted:
- Group update messages (
GroupUpdatedcontent type) - Membership change messages (transcript messages)
- Any message where
kind == GroupMessageKind::MembershipChange - Retractions canât be deleted!
Attempting to delete these messages MUST return an error: CannotDeleteTranscriptMessage
Error types
New error types:
#[derive(Debug, Error)]
pub enum DeleteMessageError {
#[error("Message not found: {0}")]
MessageNotFound(String),
#[error("Not authorized to delete this message")]
NotAuthorizedToDelete,
#[error("Cannot delete transcript/membership change messages")]
CannotDeleteTranscriptMessage,
#[error("Message already deleted")]
MessageAlreadyDeleted,
}
API examples
// Delete a message (sender or super admin)
match conversation.delete_message(message_id).await {
Ok(_) => {
// Message deletion initiated successfully
}
Err(DeleteMessageError::NotAuthorizedToDelete) => {
// Handle authorization error
}
Err(DeleteMessageError::CannotDeleteTranscriptMessage) => {
// Handle transcript message error
}
Err(e) => {
// Handle other errors
}
}
// Query messages (deletions applied automatically)
let messages = conversation.find_messages(None).await?;
for message in messages {
match message.content {
MessageBody::Text(text) => display_message(&text.content),
MessageBody::DeletedMessage { deleted_by } => {
match deleted_by {
DeletedBy::Sender => show_placeholder("Message deleted"),
DeletedBy::Admin(admin_inbox_id) => {
show_placeholder(&format!("Message deleted by admin: {}", admin_inbox_id))
}
}
}
_ => {
// Handle other message types
}
}
}
// Conversation list with deleted last message
let conversations = client.conversations().list(None).await?;
for item in conversations {
if let Some(last_message) = item.last_message {
if matches!(last_message.content, MessageBody::DeletedMessage { .. }) {
show_preview("Message deleted");
}
}
}
Rationale
Non-destructive deletion approach
This XIP deliberately avoids modifying or removing original messages from the database. Instead, both the original message and the DeleteMessage remain stored, with deletion applied at query time through a separate message_deletions table. This approach is necessary because in the decentralized XMTP network, a DeleteMessage may arrive before the original message it referencesârequiring deletion records to exist independently. Additionally, maintaining immutable message records with separate deletion metadata preserves a complete audit trail for moderation purposes while simplifying replication and synchronization across the distributed network.
Future iterations
V2: Message retraction
A future version will support message retraction where:
- The
DeleteMessageis kept ingroup_messagesandmessage_deletionstables (same as V1) - The original messageâs encrypted content is retracted (removed) from the database
- Message metadata is preserved (message ID, sender
inbox_id, senderinstallation_id, timestamp, etc.) - The message remains visible in queries but the body is permanently removed
- A background worker process will retract messages that arrived before the deletion
- For messages that arrive after a
DeleteMessage, they are retracted immediately upon arrival - This provides stronger deletion semantics while maintaining audit trail and message continuity
Admin permissions
Currently, only super admins can delete any message. Future versions will:
- Introduce a
DELETE_MESSAGESpermission - Allow super admins to grant this permission to regular admins
- Enable role-based message moderation
Smart conversation previews
Enhance conversation list to:
- Traverse backwards to find the last non-deleted message
- Display the most recent visible message as preview
- Update counts to exclude deleted messages
Bulk deletion
Enable deletion of multiple messages:
- Batch delete by message ID array
- Delete by time range
- Delete by sender
Backward compatibility
Clients that do not support the DeleteMessage content type:
- Will see the delete message as an unknown content type
- Will NOT have their local messages deleted
- Will continue to display the original message content
This is expected behavior as deletion is a best-effort mechanism that requires client cooperation.
Security considerations
Not a security feature
[!IMPORTANT]
Message deletion is NOT a security or privacy feature.
Users MUST be informed that:
-
No guarantee of deletion: Once a message is delivered and decrypted by a recipient, there is no technical guarantee that it will be deleted from their device.
-
Custom clients: Recipients using custom or malicious clients may:
- Ignore
DeleteMessagerequests entirely - Cache message content before deletion
- Export or backup messages externally
- Ignore
-
Message history: Recipients may have:
- Taken screenshots before deletion
- Copied message content
- Forwarded messages to other conversations
-
Network retention: The original message remains on XMTP network nodes until expiration (currently 6 months) and is not deleted by this mechanism.
Original message preservation
The deletion mechanism does NOT remove the original message from:
- Local databases (the encrypted message bytes remain)
- Network nodes
- Backup systems
It only affects the presentation layer, replacing content with a placeholder in queries.
Authorization validation
Super admin status MUST be validated at the time of deletion, not at query time:
- When receiving a
DeleteMessage, check if sender has super admin privileges - Store the
deleted_by_super_adminflag based on this real-time check - This prevents privilege escalation if super admin status is later revoked
Audit trail
Implementations SHOULD maintain audit logs of deletions:
- Who deleted the message
- When it was deleted
- Whether it was a super admin deletion
- Original message metadata (sender, timestamp)
Threat model
Unauthorized deletion by non-privileged members: A malicious group member attempts to delete messages sent by other users without having super admin privileges. The system prevents this by validating authorization at deletion timeâonly the original sender or a current super admin can delete a message.
Privilege escalation through delayed deletion: An attacker who was temporarily a super admin attempts to delete messages after their admin privileges have been revoked. The system mitigates this by checking super admin status at the time the DeleteMessage is received and storing the is_super_admin_deletion flag based on real-time validation, not query-time checks.
Deletion of critical audit trail messages: An attacker attempts to delete transcript messages or membership changes to hide evidence or disrupt group history. The system explicitly prohibits deletion of GroupUpdated content types and messages with kind == GroupMessageKind::MembershipChange, returning CannotDeleteTranscriptMessage errors.
Fabricated deletion records without valid signatures: A malicious server or client attempts to inject fake deletion records without proper authentication. The system prevents this because all DeleteMessages must be valid, properly signed MLS messages before creating entries in the message_deletions table.
Malicious clients ignoring deletion requests: Recipients using custom or malicious clients may ignore DeleteMessage requests entirely, cache message content before deletion, or export messages externally. This is an acknowledged limitationâmessage deletion is a best-effort mechanism that requires client cooperation and cannot guarantee removal from all devices.
Message retention through cached databases: Recipients may have database backups, cached data, or offline copies that preserve deleted message content. The deletion mechanism only affects the presentation layer in cooperating clients and does not remove the original encrypted message bytes from local databases or network nodes.
Privacy considerations
Users should be informed through UI/UX that:
- Deleted messages show a placeholder indicating deletion
- The sender or super admin who deleted the message may be visible
- Deletion does not guarantee removal from all recipients
- Message metadata remains accessible
Implementation notes
Enrichment layer
The message enrichment process (similar to reactions and replies in enrichment.rs) should:
- Load deletion records for the queried message set
- Apply deletion filtering during the enrichment phase
- Replace message content with
DeletedMessageplaceholders - Maintain consistent behavior across all query paths
Message processing flow
The following diagram illustrates how delete messages are processed, including out-of-order delivery scenarios:
sequenceDiagram
participant Client as Receiving Client
participant DB as Database
participant Query as Query Engine
Note over Client,Query: Scenario 1: Delete arrives BEFORE original message
Client->>DB: Receive DeleteMessage for msg_123
DB->>DB: Check if msg_123 exists
Note over DB: msg_123 NOT FOUND
DB->>DB: Store deletion in message_deletions<br/>(deleted_message_id = msg_123)
Client->>DB: Later: Receive original msg_123
DB->>DB: Store msg_123 in group_messages
Query->>DB: Query messages
DB->>DB: JOIN with message_deletions
Note over DB: msg_123 has deletion record
DB-->>Query: Return msg_123 as DeletedMessage
Note over Client,Query: Scenario 2: Original message arrives BEFORE delete
Client->>DB: Receive original msg_456
DB->>DB: Store msg_456 in group_messages
Client->>DB: Later: Receive DeleteMessage for msg_456
DB->>DB: Check if msg_456 exists
Note over DB: msg_456 FOUND
DB->>DB: Validate authorization<br/>(sender or super admin)
DB->>DB: Store deletion in message_deletions
Query->>DB: Query messages
DB->>DB: JOIN with message_deletions
Note over DB: msg_456 has deletion record
DB-->>Query: Return msg_456 as DeletedMessage
Database indexes
Indexes are already defined in the schema above:
CREATE INDEX idx_message_deletions_deleted_message_id ON message_deletions(deleted_message_id);
CREATE INDEX idx_message_deletions_group_id ON message_deletions(group_id);
These indexes enable efficient:
- Lookup of deletions when querying messages
- Checking if an incoming message has a pending deletion
- Group-wide deletion queries for pagination
Test cases
Tests MUST cover:
- Original sender can delete their own message
- Super admin can delete any message
- Regular members cannot delete othersâ messages
- Transcript messages cannot be deleted
- Deleted messages appear as placeholders in queries
- Pagination correctly applies deletions
- Conversation list handles deleted last message
- Out-of-order delivery scenarios:
DeleteMessagearrives before the original message- Original message arrives after
DeleteMessage(must still appear deleted) - Multiple
DeleteMessages for the same original message DeleteMessagereferences a message that never arrives (orphaned deletion)
Copyright
Copyright and related rights waived via CC0.