You can update and delete messages that have been published to a channel, for use cases such as:
- Message editing - allow users to edit their messages in chat-like applications
- Content moderation - remove or edit inappropriate content after publication
- Gradual message building - a message can be published while still unfinished, and then repeatedly edited with more complete information, so that someone querying history once the message is complete will only see the final version
Updating or deleting a message does not modify any messages that have been received by subscribing clients in-place: a given Message object is immutable. Rather, it publishes a new message to the channel, with an action of message.update or message.delete, with the same serial as the original message, that subscribing clients can see and act on.
It also replaces the original message in message history, so history queries will see the latest version of the message (but in the place in history of the original).
You can specify metadata (such as the reason for the update and which client is doing the update), which is published along with it.
You can access the full version history of any given message.
Enable message updates and deletes
Message updates and deletes can be enabled for a channel or channel namespace with the Message annotations, updates, deletes, and appends channel rule.
- Go to the Settings tab of an app in your dashboard.
- Under channel rules, click Add new rule.
- Enter the channel name or channel namespace on which to enable message updates and deletes.
- Check Message annotations, updates, deletes, and appends to enable the feature.
- Click Create channel rule to save.
Update a message
To update an existing message, use the updateMessage() method on a REST or realtime channel. The published update will have an action of message.update.
The message is identified by its serial, which is populated by Ably. To update a message, you need its serial - you can get this either from the return value of publish(), from a received message via subscription, or by querying history.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const realtime = new Ably.Realtime({ key: 'demokey:*****' });
// This assumes there is an 'updates' namespace with a channel rule enabling updates and deletes
const channel = realtime.channels.get('updates:example');
// Publish the original message and get its serial from the result
const publishResult = await channel.publish({
name: 'message-name',
data: 'original-data',
});
const serial = publishResult.serials[0];
// Publish an update using the serial
await channel.updateMessage(
{
serial,
data: 'updated-data'
},
{ description: 'reason for update' }
);Returns
Returns an UpdateDeleteResult, an object with a single versionSerial field: the serial of the version of the updated message, or null if the message was superseded by a subsequent update before it could be published.
Mixin semantics
When updating a message, any data, name, and extras you specify in the update will replace the corresponding fields in the existing message. Any you leave out remain as they were, so you get a shallow mixin. For example, if a message has { name: "greeting", data: "hello" }, and you update it with { data: "hi" }, the result will be { name: "greeting", data: "hi" }.
The fields that can be updated are:
datanameextras
Conflation
Ably may opportunistically discard out of date updates to a given message, for example, during a serverside batching step, or within a rewind backlog. This means subscribers are not guaranteed to receive every intermediate update if multiple updates occur in quick succession, but it is guaranteed that the last update that they receive will represent the most recent version of the message (matching the version that will be eventually retrievable by a history or getMessage() request).
Capabilities
To update messages, clients need one of the following capabilities:
| Capability | Description |
|---|---|
| message-update-own | Can update your own messages (more precisely, messages where the original publisher's clientId matches the updater's clientId, where both are identified). |
| message-update-any | Can update any message on the channel. |
Operation metadata
When updating a message, you can optionally provide metadata about the update operation:
| Property | Description | Type |
|---|---|---|
| clientId | The client identifier of the user performing the update (automatically populated if the delete is done by an identified client). | String |
| description | A description of why the update was made. | String |
| metadata | Additional metadata about the update operation. | Object |
This metadata will end up in the message's version property. See Message version structure for what this looks like.
Delete a message
To delete a message, use the deleteMessage() method on a REST or realtime channel. This is very much a 'soft' delete: it's just an update, but with an action of message.delete instead of message.update. It's up to your application to interpret it.
The latest version of each message will be accessible from history, including if that latest version happens to be a delete.
The message is identified by its serial, which is populated by Ably. To delete a message, you need its serial - you can get this either from the return value of publish(), from a received message via subscription, or by querying history.
Deleting a message marks it as deleted without removing it from the server. The full message history remains accessible through the message versions API.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const realtime = new Ably.Realtime({ key: 'demokey:*****' });
// This assumes there is an 'updates' namespace with a channel rule enabling updates and deletes
const channel = realtime.channels.get('updates:example');
// Publish the original message and get its serial from the result
const publishResult = await channel.publish({
name: 'message-name',
data: 'original-data',
});
const serial = publishResult.serials[0];
// Delete the message using the serial
await channel.deleteMessage(
{
serial,
data: '' // clear the previous data
},
{ description: 'reason for delete' }
);Returns
Returns an UpdateDeleteResult, an object with a single versionSerial field: the serial of the version of the deleted message, or null if the message was superseded by a subsequent update before it could be published.
Mixin semantics
Deleting has the same semantics as updating, so only the message fields (out of data, name, and extras) you specify in the update will replace the corresponding fields in the existing message, in a shallow mixin.
That means that if you e.g. want the deleted message to have empty data (to prevent users looking at raw history results from the API from seeing what the data used to be), you must explicitly set to e.g. an empty object when publishing the delete. (And even then, all previous versions are accessible through the version history API).
Conflation
Since deletes are just updates with a different action, as with updates, Ably may opportunistically discard out of date versions of a given message, for example, during a serverside batching step, or within a rewind backlog. This means subscribers are not guaranteed to receive every intermediate update if multiple updates/deletes occur in quick succession, but it is guaranteed that the last update/delete that they receive will represent the most recent version of the message (matching the version that will be eventually retrievable by a history or getMessage() request).
Capabilities
To delete messages, clients need one of the following capabilities:
| Capability | Description |
|---|---|
| message-delete-own | Can delete your own messages (more precisely, messages where the original publisher's clientId matches the deleter's clientId, where both are identified). |
| message-delete-any | Can delete any message on the channel. |
Operation metadata
When deleting a message, you can optionally provide metadata:
| Property | Description | Type |
|---|---|---|
| clientId | The client identifier of the user performing the delete (automatically populated if the delete is done by an identified client). | String |
| description | A description of why the delete was made. | String |
| metadata | Additional metadata about the delete operation. | Object |
This metadata will end up in the message's version property. See Message version structure for what this looks like.
Append to a message
To append data to an existing message, use the appendMessage() method on a REST or realtime channel. The published append will have an action of message.append. This is useful for building up message content incrementally, for example in streaming or gradual message building scenarios.
The message is identified by its serial, which is populated by Ably. To append to a message, you need its serial - you can get this either from the return value of publish(), from a received message via subscription, or by querying history.
When Ably receives an append, it concatenates the provided data with the current latest version to calculate a full (non-incremental) version of the message. That version (with an action of message.update) is then used in contexts like History and rewind, so that when using those APIs, you will always receive complete messages without needing to do any concatenation yourself, and can specify e.g. rewind=10 to get the most recent 10 full, distinct messages.
Comparison with update
| Aspect | updateMessage() | appendMessage() |
|---|---|---|
data field | Replaces if provided | Concatenates to the end of the most recent data |
name & extras fields | Replaces if provided | Replaces if provided |
| Version history | Stored | Not stored (designed for high-frequency updates from a single publisher) |
| Realtime subscribers | Receive latest full message | Receive incremental appends (but can request full versions via channel param) |
| History and rewind | Get latest full message | Get latest full message (fully-aggregated) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const realtime = new Ably.Realtime({ key: 'demokey:*****' });
// This assumes there is an 'updates' namespace with a channel rule enabling updates and deletes
const channel = realtime.channels.get('updates:example');
// Publish the original message and get its serial from the result
const publishResult = await channel.publish({
name: 'message-name',
data: 'Hello',
});
const serial = publishResult.serials[0];
// Append to the message a few times (without needing to await each to finish
// before doing the next); the data will be concatenated
channel.appendMessage({ serial, data: ', ' });
channel.appendMessage({ serial, data: 'World' });
channel.appendMessage({ serial, data: '!' });
// the message in history now has data: "Hello, World!"Returns
Returns an UpdateDeleteResult, an object with a single versionSerial field: the serial of the version of the appended message, or null if the message was superseded by a subsequent update before it could be published.
Ordering
You don't need to wait for one append publish before doing the next: they can be pipelined, and Ably will construct the full payload optimistically in the pipeline. (Which means there is a possibility of an append being rejected e.g. due to a channel rate limit, but its data still being optimistically incorporated into a subsequent append).
If you want to publish a high rate of appends to a single message, you should be publishing with a realtime client, since then Ably message order preservation will guarantee that the appends will be applied in the same order they were published. If using a REST client, while in practice message order is often still preserved (especially in sdks that use http/2), this cannot be guaranteed.
Conflation
Ably may opportunistically conflate multiple appends to the same message together (concatenating their data payloads), so subscribers may receive a single append containing the combined data of multiple append operations rather than each append individually. The operation metadata of this will be from the most recent of the appends.
Ably may also at any point deliver an append to subscribers as a message.update containing the complete payload so far (instead of an incremental message.append).
Capabilities
To append to messages, clients need one of the following capabilities:
| Capability | Description |
|---|---|
| message-update-own | Can append to your own messages (more precisely, messages where the original publisher's clientId matches your clientId, where both are identified). |
| message-update-any | Can append to any message on the channel. |
Operation metadata
When appending to a message, you can optionally provide metadata:
| Property | Description | Type |
|---|---|---|
| clientId | The client identifier of the user performing the append (automatically populated if done by an identified client). | String |
| description | A description of why the append was made. | String |
| metadata | Additional metadata about the append operation. | Object |
This metadata will end up in the message's version property. See Message version structure for what this looks like.
Message conflation
When server-side batching or message conflation is enabled, Ably groups update, delete, and append messages received in the configured time window before delivering them to subscribers.
During this window, if multiple operations target the same message (identified by its serial), Ably combines them into a single operation.
Instead of receiving every intermediate update, append, or delete, subscribers receive a single message representing the combined result.
For update, delete, and append messages the same behavior applies whether you use batching rules or conflation rules.
Append conflation
Ably concatenates a series of appends to the same message into a single larger append. Subscribers receive one append containing the combined data from all appends in the conflation window, with the operation metadata from the most recent append. The append messages must all have the same data type (e.g. all strings or all binary) in order to be concatenated correctly.
Update and delete conflation
When an update or delete occurs, it supersedes all previous operations for that message in the time window. If additional appends follow the update or delete within the same window, Ably combines them: subscribers receive the full message content including those appends, rather than receiving the update followed by separate append operations.
Conversely, if an update or delete arrives after appends in the conflation window, the update or delete wins and discards the pending appends.
Get the latest version of a message
To retrieve the most recent version of a specific message, use the getMessage() method on a REST channel. You can pass either the message's serial identifier as a string, or a message object with a serial property.
This operation requires the history capability.
1
2
3
4
5
6
const rest = new Ably.Rest({ key: 'demokey:*****' });
const channel = rest.channels.get('updates:example');
// could also use msg.serial, useful if you want to retrieve a
// message for a serial you have stored or passed around
const message = await channel.getMessage(msg);Get message versions
To retrieve all historical versions of a message, use the getMessageVersions() method. This returns a paginated result containing all versions of the message, including the original and all subsequent updates or delete operations, ordered by version.
This operation requires the history capability.
1
2
3
4
5
const rest = new Ably.Rest({ key: 'demokey:*****' });
const channel = rest.channels.get('updates:example');
const page = await channel.getMessageVersions(msg);
console.log(`Found ${page.items.length} versions`);Message version structure
A published update or delete contains version metadata in the version property. The following shows the structure of a message after it has been updated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
// The top-level serial is a permanent identifier of the message, and remains
// the same for all updates and deletes of that message
"serial": "01826232498871-001@abcdefghij:001",
// The clientId of the user who published the original message
"clientId": "user123",
// The timestamp of that original publish
"timestamp": 1718195879988,
// Main payload fields
"name": "greeting",
"data": "hello world (edited)",
// The action tells you if it's an original ("message.create"), update,
// delete, or annotation summary
"action": "message.update",
"version": {
// The serial of the current version. For an original (with an action of
// "message.create"), this will be equal to the top-level serial. You can
// use this to compare different versions to see which one is more recent
"serial": "01826232512345-002@abcdefghij:002",
// The clientId of the user who made the update
"clientId": "user123",
// The timestamp of this latest version
"timestamp": 1718195912345,
// Update metadata, supplied by the user who published the update
"description": "Fixed typo",
"metadata": {
"reason": "correction"
}
}
}Message actions
The action property on a message indicates the type of operation:
| Action | Description |
|---|---|
| message.create | The original message |
| message.update | An update to an existing message |
| message.delete | A deletion of a message |
| meta | A message originating from ably rather than being published by a user, such as inband occupancy events |
| message.summary | A message containing the latest rolled-up summary of annotations |
| message.append | An append to an existing message's data |
Version ordering
Both the message serial and version.serial are lexicographically sortable strings, providing a deterministic ordering of messages and their versions.
To determine which version of a message is newer, compare the version.serial values.
In the case of updates or deletes made by different users at similar times, both will be published on the channel, but the one that is assigned the lexicographically-highest version.serial will 'win', in the sense that retrieving channel history will eventually always return that version of the message.