LiveObjects provides a JavaScript REST SDK that enables you to directly work with objects without using a realtime connection.
Setup
Create an Ably.Rest client instance with the LiveObjects plugin:
1
2
3
import { LiveObjects } from 'ably/liveobjects';
const rest = new Ably.Rest({ plugins: { LiveObjects }, /* other ClientOptions */ });The LiveObjects REST SDK is then available on a channel via channel.object:
1
2
const channel = rest.channels.get('my-channel');
channel.object // LiveObjects REST SDKAuthentication
Authentication is configured when instantiating the REST client. Pass an API key or use token authentication via the ClientOptions. See the REST SDK authentication documentation for details.
To read objects on a channel, an API key must have the object-subscribe capability. With only this capability, clients have read-only access, preventing them from publishing operations.
To create or update objects, the API key must have the object-publish capability. Include both object-subscribe and object-publish for full read and write access.
Fetch objects
Use channel.object.get
get(RestObjectGetParams params?): Promise<Object>
Reads object data from the channel. If no objectId is provided then the entire channel object is returned. Makes a request to the GET /channels/{channelId}/object REST API endpoint. The return type depends on the compact parameter: when compact is true (default), returns a RestObjectGetCompactResult; when compact is false, returns a RestObjectGetFullResult.
| Parameter | Description | Type |
|---|---|---|
| params | An optional object containing the query parameters | RestObjectGetParams |
Objects can be fetched in two formats:
| Format | Parameter | Description |
|---|---|---|
| Compact (default) | compact: true | Values-only representation without metadata. Ideal for reading data values. |
| Full | compact: false | Full structure including object IDs and type metadata. Useful for debugging. |
Compact format returns the logical structure of your data as a JSON-like value. LiveMap instances appear as JSON objects with their entries, and LiveCounter instances appear as numbers. bytes-typed values are returned as ArrayBuffer when using the binary protocol, or as base64-encoded strings when using the JSON protocol. json-typed values remain as JSON-encoded strings.
Full format includes additional metadata for each object:
- Object IDs for each instance
- Object type metadata (map semantics, counter values)
- Complete object hierarchy
Since each value in the full format carries explicit type information, the SDK always decodes bytes values to ArrayBuffer and json values to native objects or arrays.
See Data values reference for more details on value types and encoding.
Fetch the channel object
Fetch the entire channel object:
1
const data = await channel.object.get();Example compact result:
1
2
3
4
5
6
{
"votes": {
"down": 5,
"up": 10
}
}This example shows a LiveMap stored on the "votes" key of the channel object, which contains two LiveCounter instances on the "down" and "up" keys.
Set compact: false to include object metadata:
1
const data = await channel.object.get({ compact: false });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
{
"objectId": "root",
"map": {
"semantics": "lww",
"entries": {
"votes": {
"data": {
"objectId": "map:qZXBk8xaGqf4kwLSlR7Tj0Eqhd48WDFfb3gTAbj194k@1760448597692",
"map": {
"semantics": "lww",
"entries": {
"down": {
"data": {
"objectId": "counter:Yj1F_aEX3T2rRkTkra7Aifmlr8PxUWSR3kO3MzxtQto@1760448653393",
"counter": {
"data": {
"number": 5
}
}
}
},
"up": {
"data": {
"objectId": "counter:ibxddWpDjH8R3cvXWWacfe4IVd3DxT_oqkAafhaS68s@1760448646413",
"counter": {
"data": {
"number": 10
}
}
}
}
}
}
}
}
}
}
}Fetch by path
Return a subset of the channel object by specifying the path parameter. For example, to return only the votes LiveMap instance from the channel object:
1
const data = await channel.object.get({ path: 'votes' });Example result:
1
2
3
4
{
"down": 5,
"up": 10
}Fetch by object ID
Fetch a specific object instance by specifying its objectId:
1
2
3
const data = await channel.object.get({
objectId: 'map:qZXBk8xaGqf4kwLSlR7Tj0Eqhd48WDFfb3gTAbj194k@1760448597692'
});Example compact result:
1
2
3
4
{
"down": 5,
"up": 10
}Set compact: false to include object metadata:
1
2
3
4
const data = await channel.object.get({
objectId: 'map:qZXBk8xaGqf4kwLSlR7Tj0Eqhd48WDFfb3gTAbj194k@1760448597692',
compact: false
});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
{
"objectId": "map:qZXBk8xaGqf4kwLSlR7Tj0Eqhd48WDFfb3gTAbj194k@1760448597692",
"map": {
"semantics": "lww",
"entries": {
"down": {
"data": {
"objectId": "counter:Yj1F_aEX3T2rRkTkra7Aifmlr8PxUWSR3kO3MzxtQto@1760448653393",
"counter": {
"data": {
"number": 5
}
}
}
},
"up": {
"data": {
"objectId": "counter:ibxddWpDjH8R3cvXWWacfe4IVd3DxT_oqkAafhaS68s@1760448646413",
"counter": {
"data": {
"number": 10
}
}
}
}
}
}
}You can also specify a path parameter alongside objectId. The path is evaluated relative to the specified object instance:
1
2
3
4
const data = await channel.object.get({
objectId: 'map:qZXBk8xaGqf4kwLSlR7Tj0Eqhd48WDFfb3gTAbj194k@1760448597692',
path: 'down'
});1
5Publish operations
Use channel.object.publish
publish(RestObjectOperation operation): Promise<RestObjectPublishResult>
publish(RestObjectOperation[] operations): Promise<RestObjectPublishResult>
Publishes one or more operations to modify objects on the channel. Makes a request to the POST /channels/{channelId}/object REST API endpoint. When an array is provided, all operations are published as an atomic batch.
| Parameter | Description | Type |
|---|---|---|
| operation | The operation or array of operations to publish | RestObjectOperation or RestObjectOperation[] |
Returns a RestObjectPublishResult containing the message ID and affected object IDs.
Each operation includes:
- A reference to an object using either
objectIdorpath. - An operation-specific field (
mapSet,counterInc, etc.) containing the operation parameters
Operations can target objects using objectId or path:
objectId(string): The unique identifier of the object instance to create or updatepath(string): The path to the object instance within the channel object
Use dot-separated notation for paths (for example votes.up), relative to the channel object. An empty path "" refers to the channel object itself. Paths can contain wildcards (*) to target multiple objects.
Available operations
LiveObjects supports the following operations:
| Operation | Description |
|---|---|
mapSet | Sets a key/value pair in a LiveMap. |
mapRemove | Removes a key from a LiveMap. |
counterInc | Increments or decrements a LiveCounter. |
mapCreate | Creates a new LiveMap instance. |
counterCreate | Creates a new LiveCounter instance. |
mapCreateWithObjectId | Creates a new LiveMap with a client-generated ID. |
counterCreateWithObjectId | Creates a new LiveCounter with a client-generated ID. |
To create an object, see Create objects.
Each operation has specific required and optional fields:
mapSet
1
2
3
4
5
6
7
await channel.object.publish({
path: 'user',
mapSet: {
key: 'username',
value: { string: 'alice' }
}
});Map values can be any of the supported data value types, including references to other objects.
mapRemove
1
2
3
4
5
6
await channel.object.publish({
path: 'user',
mapRemove: {
key: 'username'
}
});counterInc
1
2
3
4
5
6
await channel.object.publish({
path: 'votes.up',
counterInc: {
number: 5
}
});mapCreate
Optionally omit the path or objectId fields when creating an object with mapCreate.
For the object to be reachable in the state tree, assign it to a key in a LiveMap that is reachable from the channel object.
1
2
3
4
5
6
7
8
9
10
11
await channel.object.publish({
path: 'posts.post1',
mapCreate: {
semantics: 'lww',
entries: {
title: { data: { string: 'LiveObjects is awesome' } },
createdAt: { data: { number: 1745835181122 } },
isPublished: { data: { boolean: true } }
}
}
});counterCreate
Optionally omit the path or objectId fields when creating an object with counterCreate.
For the object to be reachable in the state tree, assign it to a key in a LiveMap that is reachable from the channel object.
1
2
3
4
5
6
await channel.object.publish({
path: 'visits',
counterCreate: {
count: 0
}
});Update by object ID
To perform operations on a specific object instance, provide its objectId in the operation:
1
2
3
4
5
6
const result = await channel.object.publish({
objectId: 'counter:iVji62_MW_j4dShuJbr2fmsP2D8MyCs6tFqON9-xAkc@1745828645269',
counterInc: {
number: 1
}
});Update by path
Path operations target objects based on their location in the channel object.
Paths are expressed relative to the structure of the object as defined by the compact view of the channel object.
The following example increments the LiveCounter instance stored at the up key on the votes LiveMap object:
1
2
3
4
5
6
const result = await channel.object.publish({
path: 'votes.up',
counterInc: {
number: 1
}
});Publish result
The result includes the ID of the published operation message, the channel and a list of object IDs that were affected by the operation:
1
2
3
4
5
{
"messageId": "TJPWHhMTrF:0",
"channel": "my-channel",
"objectIds": ["counter:iVji62_MW_j4dShuJbr2fmsP2D8MyCs6tFqON9-xAkc@1745828645269"]
}Path operations
Path operations provide flexibility when targeting objects.
Path wildcards
Use wildcards in paths to target multiple objects at once. To increment all LiveCounter instances in the votes LiveMap instance:
1
2
3
4
5
6
const result = await channel.object.publish({
path: 'votes.*',
counterInc: {
number: 1
}
});The result includes the IDs of each of the affected object instances:
1
2
3
4
5
6
7
8
{
"messageId": "0Q1w-LpA11:0",
"channel": "my-channel",
"objectIds": [
"counter:iVji62_MW_j4dShuJbr2fmsP2D8MyCs6tFqON9-xAkc@1745828645269",
"counter:JbZYiHnw0ORAyzzLSQahVik31iBDL_ehJNpTEF3qwg8@1745828651669"
]
}Wildcards match exactly one level in the channel object and can appear at the end or middle of paths. For example, given the following compact view of the channel object:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"posts": {
"post1": {
"votes": {
"down": 5,
"up": 10
}
},
"post2": {
"votes": {
"down": 5,
"up": 10
}
}
}
}The following example increments the upvote LiveCounter instances for all posts in the posts LiveMap instance:
1
2
3
4
5
6
const result = await channel.object.publish({
path: 'posts.*.votes.up',
counterInc: {
number: 1
}
});Escape special characters
If your LiveMap keys contain periods, escape them with a backslash. The following example increments the upvote LiveCounter instance for a post with the key post.123:
1
2
3
4
5
6
const result = await channel.object.publish({
path: 'posts.post\\.123.votes.up',
counterInc: {
number: 1
}
});Remove objects
Remove an object from the channel using mapRemove to delete the key referencing it:
1
2
3
4
5
6
await channel.object.publish({
objectId: 'root',
mapRemove: {
key: 'posts'
}
});Map keys can be removed by issuing a mapRemove operation targeting either an objectId or a path.
If no other references to the object exist, it becomes unreachable and is eligible for garbage collection.
Create objects
Use mapCreate and counterCreate operations to create new LiveObjects instances.
Create objects with paths
The simplest way to create an object is to specify a path where it should be created. The server automatically creates the object and assigns it to that path in a single atomic operation.
The following example creates a new LiveMap instance and assigns it to the posts LiveMap instance on the channel object under the key post1:
1
2
3
4
5
6
7
8
9
10
11
const result = await channel.object.publish({
path: 'posts.post1',
mapCreate: {
semantics: 'lww',
entries: {
title: { data: { string: 'LiveObjects is awesome' } },
createdAt: { data: { number: 1745835181122 } },
isPublished: { data: { boolean: true } }
}
}
});When using path with a create operation, the server constructs two operations published as a batch:
- A
mapCreateorcounterCreateoperation to create the new object - A
mapSetoperation to assign the new object to the parentLiveMapat the specified path
This ensures the new object is immediately reachable from the channel object.
The result will include the object IDs of all objects affected by the resulting set of operations. The newly created object's ID will be the first item in the list:
1
2
3
4
5
6
7
8
{
"messageId": "mkfjWU2jju:0",
"channel": "my-channel",
"objectIds": [
"map:cRCKx-eev7Tl66jGfl1SkZh_uEMo6F5jyV0B7mUn4Zs@1745835549101",
"map:a_oQqPYUGxi95_Cn0pWcsoeBlHZZtVW5xKIw0hnJCZs@1745835547258"
]
}Client-generated object IDs
Client-generated object IDs enable atomic batch operations with cross-references between newly created objects. Use channel.object.generateObjectId to generate an object ID, then use mapCreateWithObjectId or counterCreateWithObjectId to create the object with that ID. See the REST API documentation for details on the ID generation algorithm.
Use channel.object.generateObjectId
generateObjectId(RestObjectOperationMapCreateBody | RestObjectOperationCounterCreateBody createBody): Promise<RestObjectGenerateIdResult>
Generates an object ID for a create operation. Pass a RestObjectOperationMapCreateBody or RestObjectOperationCounterCreateBody to specify the object type and initial value. Returns a RestObjectGenerateIdResult containing the generated objectId, nonce, and initialValue needed to construct a mapCreateWithObjectId or counterCreateWithObjectId operation.
The following example generates an object ID for a new map, then creates and assigns it to the channel object in a single atomic batch:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Generate an object ID for a new map
const { objectId, nonce, initialValue } = await channel.object.generateObjectId({
mapCreate: {
semantics: 'lww',
entries: {
name: { data: { string: 'Alice' } }
}
}
});
// Create the map and assign it to the channel object atomically
const result = await channel.object.publish([
{
objectId,
mapCreateWithObjectId: { initialValue, nonce }
},
{
objectId: 'root',
mapSet: {
key: 'alice',
value: { objectId }
}
}
]);Both operations execute atomically. The second operation references the object created in the first because the ID was pre-computed with generateObjectId.
Cyclic references
For both the full object and the compact formats, cyclic references in the channel object are included as a reference to the object ID rather than including the same object instance in the result more than once.
For example, if you create a cycle in the channel object by adding a reference to the channel object in the votes LiveMap instance with the following operation:
1
2
3
4
5
6
7
await channel.object.publish({
objectId: 'map:qZXBk8xaGqf4kwLSlR7Tj0Eqhd48WDFfb3gTAbj194k@1760448597692',
mapSet: {
key: 'myObject',
value: { objectId: 'root' }
}
});The result will handle the cyclic reference by including the myObject key as a reference to the object ID of the channel object:
1
const data = await channel.object.get();1
2
3
4
5
6
7
8
9
{
"votes": {
"down": 5,
"up": 10,
"myObject": {
"objectId": "root"
}
}
}Object reachability and garbage collection
Objects that are not reachable from the channel object will be automatically garbage collected. See the REST API documentation and reachability and object lifecycle for details.
Batch operations
Group multiple operations into a single call by passing an array of operations to publish(). All operations are published as a single message and processed as a single atomic unit. Learn more about batch operations.
The following example increments two distinct LiveCounter instances in a single batch operation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
await channel.object.publish([
{
objectId: 'counter:iVji62_MW_j4dShuJbr2fmsP2D8MyCs6tFqON9-xAkc@1745828645269',
counterInc: {
number: 1
}
},
{
objectId: 'counter:JbZYiHnw0ORAyzzLSQahVik31iBDL_ehJNpTEF3qwg8@1745828651669',
counterInc: {
number: 1
}
}
]);Idempotent operations
Publish operations idempotently by specifying an id for the operation, using the same approach as idempotent message publishing:
1
2
3
4
5
6
7
await channel.object.publish({
id: 'my-idempotency-key',
objectId: 'counter:iVji62_MW_j4dShuJbr2fmsP2D8MyCs6tFqON9-xAkc@1745828645269',
counterInc: {
number: 1
}
});For batch operations, use the format <baseId>:<index> where the index is the zero-based index of the operation in the array:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
await channel.object.publish([
{
id: 'my-idempotency-key:0',
objectId: 'counter:iVji62_MW_j4dShuJbr2fmsP2D8MyCs6tFqON9-xAkc@1745828645269',
counterInc: {
number: 1
}
},
{
id: 'my-idempotency-key:1',
objectId: 'counter:JbZYiHnw0ORAyzzLSQahVik31iBDL_ehJNpTEF3qwg8@1745828651669',
counterInc: {
number: 1
}
}
]);Data values reference
When working with objects via the REST SDK, primitive types and object references are represented as typed value objects.
The key in the value object indicates the type of the value. Examples of data value formats:
1
2
3
4
5
6
{ number: 42 }
{ string: 'LiveObjects is awesome' }
{ boolean: true }
{ bytes: new Uint8Array([76, 105, 118, 101]) }
{ objectId: 'counter:JbZYiHnw0ORAyzzLSQahVik31iBDL_ehJNpTEF3qwg8@1745828651669' }
{ json: { someKey: 'someValue' } }Related types
RestObjectGetParams
Parameters for the get() method:
| Property | Description | Type |
|---|---|---|
| objectId | The unique identifier of the object instance to fetch. If omitted, fetches from the channel object | String (optional) |
| path | A dot-separated path to return a subset of the object. Evaluated relative to the channel object or the specified objectId | String (optional) |
| compact | When true (default), returns a values-only representation. When false, includes object IDs and type metadata | Boolean (optional) |
RestObjectOperation
An operation passed to the publish() method. Each operation contains an optional id for idempotent publishing, an optional target (objectId or path), and exactly one operation-specific field containing the operation parameters:
| Property | Description | Type |
|---|---|---|
| id | Identifier for idempotent publishing | String (optional) |
| objectId | The unique identifier of the object instance to target | String (optional) |
| path | The dot-separated path to the object instance within the channel object. Evaluated relative to the channel object | String (optional) |
| mapSet | Parameters for setting a key to a value in a LiveMap | RestObjectOperationMapSet (optional) |
| mapRemove | Parameters for removing a key from a LiveMap | RestObjectOperationMapRemove (optional) |
| counterInc | Parameters for incrementing a LiveCounter | RestObjectOperationCounterInc (optional) |
| mapCreate | Parameters for creating a new LiveMap | RestObjectOperationMapCreate (optional) |
| counterCreate | Parameters for creating a new LiveCounter | RestObjectOperationCounterCreate (optional) |
| mapCreateWithObjectId | Parameters for creating a new LiveMap with a client-generated ID | RestObjectOperationMapCreateWithObjectId (optional) |
| counterCreateWithObjectId | Parameters for creating a new LiveCounter with a client-generated ID | RestObjectOperationCounterCreateWithObjectId (optional) |
RestObjectOperationMapSet
| Property | Description | Type |
|---|---|---|
| key | The key to set | String |
| value | The value to assign to the key | PublishObjectData |
RestObjectOperationMapRemove
| Property | Description | Type |
|---|---|---|
| key | The key to remove | String |
RestObjectOperationCounterInc
| Property | Description | Type |
|---|---|---|
| number | The amount to increment by. Use a negative value to decrement | Number |
RestObjectOperationMapCreate
| Property | Description | Type |
|---|---|---|
| semantics | The conflict-resolution semantics for the map. One of: 'lww' | String |
| entries | Initial key-value pairs, keyed by string | Record<String, { data: PublishObjectData }> |
RestObjectOperationCounterCreate
| Property | Description | Type |
|---|---|---|
| count | The initial value of the counter | Number |
RestObjectOperationMapCreateWithObjectId
Use generateObjectId() to get the initialValue and nonce values for this operation.
| Property | Description | Type |
|---|---|---|
| initialValue | JSON-encoded string of the mapCreate object. Binary values in entries must be base64-encoded | String |
| nonce | Random string used to generate the object ID | String |
RestObjectOperationCounterCreateWithObjectId
Use generateObjectId() to get the initialValue and nonce values for this operation.
| Property | Description | Type |
|---|---|---|
| initialValue | JSON-encoded string of the counterCreate object | String |
| nonce | Random string used to generate the object ID | String |
RestObjectGetCompactResult
Returned by get() when compact is true (default). The result is a recursive type: null, boolean, number, string, binary data (ArrayBuffer when using the binary protocol, or a base64-encoded string when using the JSON protocol), or a JSON object whose values are themselves RestObjectGetCompactResult. LiveMap instances appear as JSON objects with their entries, and LiveCounter instances appear as numbers.
RestObjectGetFullResult
Returned by get() when compact is false. Can be a RestLiveObject (full object with metadata) or a RestObjectData (leaf value when the path resolves to a primitive entry in a map).
RestLiveObject
A full object structure including object IDs and type metadata. Can be a RestLiveMap, RestLiveCounter, or a generic object with an objectId property.
RestLiveMap
| Property | Description | Type |
|---|---|---|
| objectId | The ID of the map object | String |
| map | The map data | RestLiveMapValue |
RestLiveMapValue
| Property | Description | Type |
|---|---|---|
| semantics | The conflict-resolution semantics. One of: 'lww' | String |
| entries | The map entries, indexed by key. Each entry is either a RestObjectDataMapEntry (leaf value) or a RestLiveObjectMapEntry (nested object) | Record<String, RestObjectDataMapEntry | RestLiveObjectMapEntry> |
RestObjectDataMapEntry
| Property | Description | Type |
|---|---|---|
| data | The value for this entry | RestObjectData |
RestLiveObjectMapEntry
| Property | Description | Type |
|---|---|---|
| data | A nested object | RestLiveObject |
RestLiveCounter
| Property | Description | Type |
|---|---|---|
| objectId | The ID of the counter object | String |
| counter | The counter data | RestLiveCounterValue |
RestLiveCounterValue
| Property | Description | Type |
|---|---|---|
| data | Holds the counter value | { number: Number } |
RestObjectData
Represents a leaf data value for an object. Either a primitive value or a reference to another object. Exactly one property is set, indicating the type:
| Property | Description | Type |
|---|---|---|
| string | A string value | String |
| number | A numeric value | Number |
| boolean | A boolean value | Boolean |
| bytes | A binary value | ArrayBuffer |
| json | A JSON value (array or object) | Array / Object |
| objectId | A reference to another object by its ID | String |
PublishObjectData
Same properties as RestObjectData. Used when publishing operations via publish(). Exactly one property must be set.
RestObjectPublishResult
Result returned by the publish() method:
| Property | Description | Type |
|---|---|---|
| messageId | The ID of the message containing the published operations | String |
| channel | The name of the channel the operations were published to | String |
| objectIds | Array of object IDs affected by the operations. May include multiple IDs for wildcard paths and batch operations | String[] |
RestObjectOperationMapCreateBody
| Property | Description | Type |
|---|---|---|
| mapCreate | Parameters for creating a new LiveMap | RestObjectOperationMapCreate |
RestObjectOperationCounterCreateBody
| Property | Description | Type |
|---|---|---|
| counterCreate | Parameters for creating a new LiveCounter | RestObjectOperationCounterCreate |
RestObjectGenerateIdResult
Result returned by generateObjectId():
| Property | Description | Type |
|---|---|---|
| objectId | The generated object ID | String |
| nonce | The nonce used in ID generation | String |
| initialValue | The JSON-encoded initial value used in ID generation | String |