---
source_url: https://www.pubnub.com/docs/sdks/javascript/api-reference/data-sync
title: DataSync API for JavaScript SDK
updated_at: 2026-09-08T17:09:13.000Z
sdk_name: PubNub JavaScript SDK
sdk_version: 13.0.0
---

# DataSync API for JavaScript SDK

PubNub JavaScript SDK, use the latest version: 13.0.0

Install:

```bash
npm install pubnub@13.0.0
```

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

[DataSync](https://www.pubnub.com/docs/general/data-sync/overview.md) is PubNub's data layer for storing application state (users, channels, memberships, and any custom object type) and keeping every connected client current through real-time events. Use it to model the objects your application works with and to react the moment they change.

DataSync is the successor to [App Context](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects.md).

:::note DataSync entities and SDK entities are different things
This page is about **DataSync entities**, the records the service stores and treats as the source of truth for your application state. They are created and read through the `pubnub.dataSync.*` methods documented here.
An **SDK entity** is a local client-side handle such as `pubnub.channel('room-1')`. It carries no stored state of its own, it just gives you a scoped surface for subscribing and calling APIs. Refer to [SDK entities](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe.md#sdk-entities) in Publish and subscribe.
The two meet in the [DataSync SDK entities](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe.md#datasync-sdk-entities): SDK entities whose only job is to subscribe to a DataSync object's real-time updates.
:::

The classes that your objects conform to (their types and schemas) are defined through the Admin API or the [Admin Portal](https://admin.pubnub.com/), not through this SDK:

* Entity classes, which back users, channels, and custom entities: [list](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), [read](https://www.pubnub.com/docs/admin-api/get-entity-class-by-id.md), [create](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md), [replace](https://www.pubnub.com/docs/admin-api/update-entity-class-with-complete-resource-replacement.md), and [delete](https://www.pubnub.com/docs/admin-api/delete-entity-class-by-id.md). Partial updates aren't implemented; replace the complete class version instead.
* Relationship classes, which back memberships and custom relationships: [list](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md), [read](https://www.pubnub.com/docs/admin-api/get-relationship-class-by-name-and-version.md), [create](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md), [replace](https://www.pubnub.com/docs/admin-api/update-relationship-class-with-complete-resource-replacement.md), and [delete](https://www.pubnub.com/docs/admin-api/delete-relationship-class-by-name-and-version.md).

A class definition is what decides, for every object of that class, which `payload` fields are filterable and sortable, which [projection](https://www.pubnub.com/docs/sdks/javascript/api-reference/access-manager.md#grant-token) each field belongs to, and how long the object lives before it expires. Refer to [managing classes](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#managing-classes) for more information.

Every response includes the HTTP `status`. The `remove*` methods return the status on its own, every other method adds a `data` key holding the object, or the array of objects for a list method, and can add a `links` object with the HATEOAS URLs the service provides (`self`, and `next` where it applies). A stored object carries its free-form `status` and its `expiresAt` auto-deletion timestamp (ISO 8601) whenever those are set on it.

###### Authorization

Every DataSync request must be [authorized](https://www.pubnub.com/docs/general/data-sync/access-control.md#authorizing-requests) with a token or signature. A request with no credential fails with a `401`; an invalid credential or a credential that doesn't permit the operation fails with a `403`.

###### Pagination

DataSync pagination is **forward-only**. There is no previous-page cursor, and no `prev_cursor`, `has_prev`, or `links.prev` in any response. To revisit an earlier page, page from the start again.

All list methods (`getUsers`, `getChannels`, `getMemberships`, `getEntities`, and `getRelationships`) accept `cursor` and `limit` (default `20`, max `100`) and can return a `meta` object with `has_next`, `next_cursor`, and `limit`. The `meta` object is optional, so guard the access when you read it, for example `response.meta?.next_cursor`.

To page forward, pass the returned `next_cursor` back as `cursor` on the next call, and stop when `has_next` is `false`. `next_cursor` is `null` on the last page. Refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination) for details.

###### Filtering and sorting

All list methods accept two mutually exclusive filter parameters, `filter` and `filterFast`. The SDK sends whatever you pass, so a call that carries both reaches the server and fails there with a `400`.

| Parameter | Consistency | Properties it can read | Expression complexity |
| --- | --- | --- | --- |
| `filterFast` | Strongly consistent, reflects the latest writes | Filtering mode `simple` or `full` | Up to 10 conditions by default (raisable per keyset) |
| `filter` | Eventually consistent, results can briefly lag writes | Filtering mode `full` only | Full expression language |

Reach for `filterFast` when the query has to see an object you just wrote, and for `filter` when you need the full expression language over a `full`-indexed property.

:::warning danger
filter
is not the strongly consistent one
The two names read the wrong way round if you assume `filter` is the basic option. `filterFast` is the strongly consistent, limited one. `filter` is the richer, eventually consistent one. Earlier builds of the SDK named these `filter` and `filterAdvanced` respectively, so a `filter` expression written against an older build now runs on the other backing store. Rename `filter` to `filterFast` and `filterAdvanced` to `filter` when you upgrade.
:::

Both parameters share the same expression language, a string built from a property name, an operator, and a value:

| Operators |  |
| --- | --- |
| Comparison | `==`, `!=`, `<`, `>`, `<=`, `>=` |
| Pattern matching | `LIKE` (case-insensitive), `SLIKE` (case-sensitive), `ILIKE` (case-insensitive, same as `LIKE`) |
| Logical | `&&`, `||`, `!`, and parentheses for grouping |

Values are quoted strings, numbers, `true`, `false`, or `null`. Pattern operators (`LIKE`, `SLIKE`, `ILIKE`) apply to string properties only. Reference a property by its `name`, not its declared `path`, and only properties declared on the class are filterable. To reach a nested `payload` property, declare it as a class property first, then filter by that property's `name`, for example `'price < 100'` for a `product` class that declares `price`.

Some examples:

```javascript
filterFast: 'price < 100'                              // strongly consistent, single condition
filterFast: '(price < 100 && stock > 0) || price > 500' // strongly consistent, grouped conditions
filter: 'name LIKE "*sneaker*"'                        // eventually consistent, needs filtering mode "full"
filter: '!(status == "discontinued")'                  // eventually consistent, negated condition
```

Both parameters otherwise only work over properties declared on the class. The built-in fields `id`, `createdAt`, and `updatedAt` are always filterable without declaring them on the class. `status` is too, unless the class redeclares `/status` scoped to projections your token cannot fully reach. Refer to [filtering](https://www.pubnub.com/docs/general/data-sync/data-operations.md#filtering) for the two filtering tiers, and [property definitions](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#property-definitions) for how properties are declared.

Use `sort` to order results. Pass an object mapping each field to a direction. A key's value is `'asc'`, `'desc'`, or `null` to apply the service default (ascending). Map multiple fields to sort by more than one, evaluated in the order the keys appear:

```javascript
sort: { price: 'desc' }                        // single field, descending
sort: { type: 'asc', price: 'desc' }           // type first, then price within each type
sort: { createdAt: null }                      // explicit service default (ascending)
```

`sort` also accepts a raw string, which the SDK passes through unchanged. List the fields separated by commas, each a property name optionally suffixed with `:asc` or `:desc`. A bare name sorts ascending:

```javascript
sort: 'price:desc'                             // single field, descending
sort: 'type,price:desc'                        // type ascending, then price descending
sort: 'createdAt:asc'                          // explicit ascending
```

The `+field` and `-field` prefixes are **not** accepted. Only properties declared on the class with a filtering mode other than `none` can be sorted on when sorting alone or alongside `filterFast`. Sorting alongside `filter` additionally requires the field to be declared `full`. The built-in fields `id`, `createdAt`, and `updatedAt` are always sortable without declaring them on the class. `status` is too, unless the class redeclares `/status` scoped to projections your token cannot fully reach.

###### Concurrency (ETag)

Every stored object carries an `eTag`. To guard against concurrent writes, pass the `eTag` you read earlier as `ifMatchesEtag` on `set*`, `update*`, and `remove*`. The SDK sends it as the `If-Match` request header.

If the server-side value has changed in the meantime, the operation fails with a `412`, and you should re-read the object and retry. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for details.

###### Partial updates

The `update*` methods apply a partial update using JSON Patch (RFC 6902). They accept `add`, `replace`, and `test` (JSON Pointer key/value maps), `remove` (an array of JSON Pointers), and `move` and `copy` (arrays of `{ from, path }` JSON Pointer pairs). Each path is a full JSON Pointer (RFC 6901) and is sent as you write it. The SDK does **not** add a `/payload` prefix. To target a field inside `payload`, include the prefix yourself, for example `replace: { '/payload/price': 79.99 }`. To target a top-level field, use a pointer from the document root, for example `replace: { '/status': 'active' }`.

Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for details.

Correct patch forms:

```javascript
add: { '/payload/tags/0': 'featured' }                 // adds a value inside payload.tags
replace: { '/payload/price': 79.99 }                   // replaces payload.price
remove: ['/payload/tempFlag', '/payload/legacy/field'] // removes payload.tempFlag and payload.legacy.field
move: [{ from: '/payload/legacyName', path: '/payload/displayName' }]
copy: [{ from: '/payload/displayName', path: '/payload/previousName' }]
test: { '/status': 'active' }                          // fails the patch if status is not "active"
```

Paths address the object's stored property names, the same keys that come back in responses. The class version is `entityClassVersion` on users, channels, and entities, and `relationshipClassVersion` on memberships and relationships. The fields set at creation cannot be patched: `entityClass` and `entityClassLevel` on users, channels, and entities, `relationshipClass`, `entityAId`, and `entityBId` on relationships, and `userId` and `channelId` on memberships.

At least one of `add`, `replace`, `remove`, `move`, `copy`, or `test` must be provided, and it must be non-empty. An empty object or array does not count. The SDK checks this before it sends anything and throws a `PubNubError` if nothing is provided.

###### Expiry (TTL)

An object's `expiresAt` is computed once when the object is created, and updates never refresh it. The value is the creation time plus the class TTL, rounded up to the start of the next whole UTC day, so it rarely lands exactly one TTL from the moment you wrote the object.

Entity classes you create default to a 31-day TTL, while the built-in Global `User` and `Channel` classes use 30 days. TTL cannot be disabled. A relationship expires at the earlier expiry time of the two entities it links, fixed when the relationship is created.

Refer to [data expiry](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#data-expiry-ttl) for details.

:::note Supported and recommended asynchronous patterns
PubNub supports [Callbacks, Promises, and Async/Await](https://javascript.info/async) for asynchronous JS operations. The recommended pattern is Async/Await and all sample requests in this document are based on it. This pattern returns a status only on detecting an error. To receive the error status, you must add the [try...catch](https://javascript.info/try-catch) syntax to your code.
:::

:::note Requires Access Manager
DataSync requires that the *Access Manager* add-on is enabled for your key in the [Admin Portal](https://admin.pubnub.com/). Read the [support page](https://support.pubnub.com/hc/en-us/articles/360051974791-How-do-I-enable-add-on-features-for-my-keys-) on enabling add-on features on your keys.
:::

## Users

Users are built-in objects. There are no top-level `name` or `email` fields, all application data lives in the free-form `payload`. Refer to [users](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#users) for the concept.

A user is an entity of the built-in `User` [entity class](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), which the service provides at the `Global` class level. To give your users their own declared, filterable properties, define a subclass of `User` with [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) and pass its name as `class`.

### Create user

Creates a user. Supply an `id` to control the identifier, or omit it to let the server generate one.

#### Method(s)

```javascript
pubnub.dataSync.createUser({
    id: string,
    class: string,
    classLevel: string,
    data: { classVersion: number, status: string, payload: object },
}): Promise<DataSync.CreateUserResponse>
```

| Parameter | Description |
| --- | --- |
| `id`Type: stringDefault: server-generated | User identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `class`Type: stringDefault: `User` | Name of the entity class this user belongs to. Must be `User` or one of its subclasses. Set at creation and immutable afterward. |
| `classLevel`Type: stringDefault: service default | Class hierarchy level of `class`, either `Global` for a class the service provides or `SubKey` for one defined on your key set. Set at creation and immutable afterward. |
| `data` *Type: objectDefault: n/a | The mutable user data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the user class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.createUser({
        id: 'user-alice',
        data: {
            classVersion: 1,
            payload: { name: 'Alice', type: 'shopper' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClassVersion": 1,
        "payload": {
            "name": "Alice",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T09:00:00.000Z",
        "eTag": "AbQdEfGhIjKlMn"
    }
}
```

### Get user

Returns a single user by `id`.

#### Method(s)

```javascript
pubnub.dataSync.getUser({
    id: string,
}): Promise<DataSync.GetUserResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | User identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getUser({ id: 'user-alice' })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClassVersion": 1,
        "payload": {
            "name": "Alice",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T09:00:00.000Z",
        "eTag": "AbQdEfGhIjKlMn"
    }
}
```

### Get all users

Returns a paginated list of users. All parameters are optional, so you can call `getUsers()` with no arguments, or pass a callback as the only argument. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```javascript
pubnub.dataSync.getUsers({
    class: string,
    classVersion: number,
    classLevel: string,
    cursor: string,
    limit: number,
    filter: string,
    filterFast: string,
    sort: object | string,
}): Promise<DataSync.GetUsersResponse>
```

| Parameter | Description |
| --- | --- |
| `class`Type: stringDefault: all user classes | Entity class name to filter by. Omit to list users across **every** user class. |
| `classVersion`Type: numberDefault: all versions | Entity class version to list. Omit to list users across **every** version of the class. |
| `classLevel`Type: stringDefault: service default | Class hierarchy level of `class`, either `Global` for a class the service provides or `SubKey` for one defined on your key set. Used to disambiguate a class name defined at both levels. |
| `cursor`Type: stringDefault: n/a | Opaque pagination cursor. Omit for the first page. |
| `limit`Type: numberDefault: `20` | Maximum number of users per page. Max `100`. |
| `filter`Type: stringDefault: n/a | Filter expression evaluated against eventually consistent storage, so results can briefly lag writes. Supports the full expression language, over properties declared with filtering mode `full`. Cannot be combined with `filterFast`. |
| `filterFast`Type: stringDefault: n/a | Filter expression evaluated against strongly consistent storage, so it reflects the latest writes. Accepts up to 10 conditions by default (raisable per keyset), over properties declared with filtering mode `simple` or `full`. Cannot be combined with `filter`. |
| `sort`Type: object | stringDefault: n/a | Order results. An object mapping field to `'asc'`, `'desc'`, or `null` (service default), for example `{ createdAt: 'desc' }`, or a comma-separated string such as `'createdAt:desc'`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getUsers({
        limit: 20,
        sort: { createdAt: 'asc' },
    })
    console.log(response.data)
    console.log(response.meta)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "user-alice",
            "entityClassVersion": 1,
            "payload": {
                "name": "Alice",
                "type": "shopper"
            },
            "createdAt": "2026-07-13T09:00:00.000Z",
            "updatedAt": "2026-07-13T09:00:00.000Z",
            "eTag": "AbQdEfGhIjKlMn"
        }
    ],
    "meta": {
        "next_cursor": "b2Zmc2V0PTIw",
        "has_next": true,
        "limit": 20
    }
}
```

#### Other examples

##### Filter with filter

`filter` runs over properties declared with filtering mode `full` and is evaluated against eventually consistent storage, so a very recent write may not be matched yet. Reference a declared property by its `name`, not its `path`.

```javascript
try {
    const response = await pubnub.dataSync.getUsers({
        filter: 'name LIKE "*Alice*"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filterFast

`filterFast` is evaluated against strongly consistent storage, so it matches an object you just wrote. It runs over properties declared with filtering mode `simple` or `full` and accepts up to 10 conditions by default (raisable per keyset). It shares the same expression language as `filter`, so only one of the two can be sent per call.

```javascript
try {
    const response = await pubnub.dataSync.getUsers({
        filterFast: 'type == "shopper"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `meta.next_cursor` from the response and pass it back as `cursor` on the next call. Stop when `meta.has_next` is `false`.

```javascript
try {
    let cursor
    let hasNext = true
    let page = 0

    while (hasNext) {
        const response = await pubnub.dataSync.getUsers({
            filterFast: 'type == "shopper"',
            limit: 20,
            cursor,
        })
        console.log(`Page ${++page}:`, response.data)
        cursor = response.meta?.next_cursor
        hasNext = response.meta?.has_next ?? false
    }
} catch (error) {
    console.log(error.status)
}
```

### Update user

Replaces a user in full (PUT). Send the complete set of fields, any field you omit is cleared. To guard against concurrent writes, pass `ifMatchesEtag` (see [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags)).

#### Method(s)

```javascript
pubnub.dataSync.setUser({
    id: string,
    data: { classVersion: number, status: string, payload: object },
    ifMatchesEtag: string,
}): Promise<DataSync.SetUserResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | User identifier. |
| `data` *Type: objectDefault: n/a | The replacement user data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the user class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.setUser({
        id: 'user-alice',
        data: {
            classVersion: 1,
            payload: { name: 'Alice B.', type: 'shopper' },
        },
        ifMatchesEtag: 'AbQdEfGhIjKlMn',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClassVersion": 1,
        "payload": {
            "name": "Alice B.",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T10:15:00.000Z",
        "eTag": "CdEfGhIjKlMnOp"
    }
}
```

### Patch user

Applies a partial update to a user. Paths can target fields inside `payload` or top-level stored fields. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the JSON Pointer rules.

#### Method(s)

```javascript
pubnub.dataSync.updateUser({
    id: string,
    add: object,
    replace: object,
    remove: string[],
    move: { from: string, path: string }[],
    copy: { from: string, path: string }[],
    test: object,
    ifMatchesEtag: string,
}): Promise<DataSync.UpdateUserResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | User identifier. |
| `add`Type: objectDefault: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
| `replace`Type: objectDefault: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
| `remove`Type: arrayDefault: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
| `move`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is removed and re-added at `path`. |
| `copy`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is duplicated to `path`. |
| `test`Type: objectDefault: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

Use a JSON Pointer that starts with `/payload/` to target payload fields, or a root pointer (for example `/status`) to target a top-level field. At least one of `add`, `replace`, `remove`, `move`, `copy`, or `test` must be provided.

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.updateUser({
        id: 'user-alice',
        replace: { '/payload/name': 'Alice B.', '/status': 'active' },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClassVersion": 1,
        "status": "active",
        "payload": {
            "name": "Alice B.",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T10:20:00.000Z",
        "eTag": "EfGhIjKlMnOpQr"
    }
}
```

### Remove user

Deletes a user by `id`.

#### Method(s)

```javascript
pubnub.dataSync.removeUser({
    id: string,
    ifMatchesEtag: string,
}): Promise<DataSync.RemoveUserResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | User identifier. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.removeUser({ id: 'user-alice' })
    console.log(response.status)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200
}
```

## Channels

Channels work like users. They have no top-level `name` field, all application data lives in `payload`, and they support the same operations. Refer to [channels](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#channels) for the concept.

A channel is an entity of the built-in `Channel` [entity class](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), which the service provides at the `Global` class level. Subclass it with [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) to add declared properties, then pass the subclass name as `class`.

### Create channel

Creates a channel. Supply an `id` to control the identifier, or omit it to let the server generate one.

#### Method(s)

```javascript
pubnub.dataSync.createChannel({
    id: string,
    class: string,
    classLevel: string,
    data: { classVersion: number, status: string, payload: object },
}): Promise<DataSync.CreateChannelResponse>
```

| Parameter | Description |
| --- | --- |
| `id`Type: stringDefault: server-generated | Channel identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `class`Type: stringDefault: `Channel` | Name of the entity class this channel belongs to. Must be `Channel` or one of its subclasses. Set at creation and immutable afterward. |
| `classLevel`Type: stringDefault: service default | Class hierarchy level of `class`, either `Global` for a class the service provides or `SubKey` for one defined on your key set. Set at creation and immutable afterward. |
| `data` *Type: objectDefault: n/a | The mutable channel data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the channel class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.createChannel({
        id: 'channel-summer-sale',
        data: {
            classVersion: 1,
            payload: { name: 'Summer Sale', type: 'promotion' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClassVersion": 1,
        "payload": {
            "name": "Summer Sale",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T09:05:00.000Z",
        "eTag": "GhIjKlMnOpQrSt"
    }
}
```

### Get channel

Returns a single channel by `id`.

#### Method(s)

```javascript
pubnub.dataSync.getChannel({
    id: string,
}): Promise<DataSync.GetChannelResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Channel identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getChannel({ id: 'channel-summer-sale' })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClassVersion": 1,
        "payload": {
            "name": "Summer Sale",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T09:05:00.000Z",
        "eTag": "GhIjKlMnOpQrSt"
    }
}
```

### Get all channels

Returns a paginated list of channels. All parameters are optional, so you can call `getChannels()` with no arguments, or pass a callback as the only argument. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```javascript
pubnub.dataSync.getChannels({
    class: string,
    classVersion: number,
    classLevel: string,
    cursor: string,
    limit: number,
    filter: string,
    filterFast: string,
    sort: object | string,
}): Promise<DataSync.GetChannelsResponse>
```

| Parameter | Description |
| --- | --- |
| `class`Type: stringDefault: all channel classes | Entity class name to filter by. Omit to list channels across **every** channel class. |
| `classVersion`Type: numberDefault: all versions | Entity class version to list. Omit to list channels across **every** version of the class. |
| `classLevel`Type: stringDefault: service default | Class hierarchy level of `class`, either `Global` for a class the service provides or `SubKey` for one defined on your key set. Used to disambiguate a class name defined at both levels. |
| `cursor`Type: stringDefault: n/a | Opaque pagination cursor. Omit for the first page. |
| `limit`Type: numberDefault: `20` | Maximum number of channels per page. Max `100`. |
| `filter`Type: stringDefault: n/a | Filter expression evaluated against eventually consistent storage, so results can briefly lag writes. Supports the full expression language, over properties declared with filtering mode `full`. Cannot be combined with `filterFast`. |
| `filterFast`Type: stringDefault: n/a | Filter expression evaluated against strongly consistent storage, so it reflects the latest writes. Accepts up to 10 conditions by default (raisable per keyset), over properties declared with filtering mode `simple` or `full`. Cannot be combined with `filter`. |
| `sort`Type: object | stringDefault: n/a | Order results. An object mapping field to `'asc'`, `'desc'`, or `null` (service default), or a comma-separated string such as `'createdAt:desc'`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getChannels({ limit: 20 })
    console.log(response.data)
    console.log(response.meta)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "channel-summer-sale",
            "entityClassVersion": 1,
            "payload": {
                "name": "Summer Sale",
                "type": "promotion"
            },
            "createdAt": "2026-07-13T09:05:00.000Z",
            "updatedAt": "2026-07-13T09:05:00.000Z",
            "eTag": "GhIjKlMnOpQrSt"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### Filter with filter

`filter` runs over properties declared with filtering mode `full` and is evaluated against eventually consistent storage, so a very recent write may not be matched yet. Reference a declared property by its `name`, not its `path`.

```javascript
try {
    const response = await pubnub.dataSync.getChannels({
        filter: 'name LIKE "*Sale*"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filterFast

`filterFast` is evaluated against strongly consistent storage, so it matches an object you just wrote. It runs over properties declared with filtering mode `simple` or `full` and accepts up to 10 conditions by default (raisable per keyset). It shares the same expression language as `filter`, so only one of the two can be sent per call.

```javascript
try {
    const response = await pubnub.dataSync.getChannels({
        filterFast: 'type == "promotion"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `meta.next_cursor` from the response and pass it back as `cursor` on the next call. Stop when `meta.has_next` is `false`.

```javascript
try {
    let cursor
    let hasNext = true
    let page = 0

    while (hasNext) {
        const response = await pubnub.dataSync.getChannels({
            filterFast: 'type == "promotion"',
            limit: 20,
            cursor,
        })
        console.log(`Page ${++page}:`, response.data)
        cursor = response.meta?.next_cursor
        hasNext = response.meta?.has_next ?? false
    }
} catch (error) {
    console.log(error.status)
}
```

### Update channel

Replaces a channel in full (PUT). Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`.

#### Method(s)

```javascript
pubnub.dataSync.setChannel({
    id: string,
    data: { classVersion: number, status: string, payload: object },
    ifMatchesEtag: string,
}): Promise<DataSync.SetChannelResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Channel identifier. |
| `data` *Type: objectDefault: n/a | The replacement channel data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the channel class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.setChannel({
        id: 'channel-summer-sale',
        data: {
            classVersion: 1,
            payload: { name: 'Summer Sale 2026', type: 'promotion' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClassVersion": 1,
        "payload": {
            "name": "Summer Sale 2026",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T11:00:00.000Z",
        "eTag": "IjKlMnOpQrStUv"
    }
}
```

### Patch channel

Applies a partial update to a channel. Paths can target fields inside `payload` or top-level stored fields. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the JSON Pointer rules.

#### Method(s)

```javascript
pubnub.dataSync.updateChannel({
    id: string,
    add: object,
    replace: object,
    remove: string[],
    move: { from: string, path: string }[],
    copy: { from: string, path: string }[],
    test: object,
    ifMatchesEtag: string,
}): Promise<DataSync.UpdateChannelResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Channel identifier. |
| `add`Type: objectDefault: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
| `replace`Type: objectDefault: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
| `remove`Type: arrayDefault: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
| `move`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is removed and re-added at `path`. |
| `copy`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is duplicated to `path`. |
| `test`Type: objectDefault: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

Use a JSON Pointer that starts with `/payload/` to target payload fields, or a root pointer (for example `/status`) to target a top-level field. At least one of `add`, `replace`, `remove`, `move`, `copy`, or `test` must be provided.

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.updateChannel({
        id: 'channel-summer-sale',
        replace: { '/payload/name': 'Summer Sale 2026' },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClassVersion": 1,
        "payload": {
            "name": "Summer Sale 2026",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T11:05:00.000Z",
        "eTag": "KlMnOpQrStUvWx"
    }
}
```

### Remove channel

Deletes a channel by `id`.

#### Method(s)

```javascript
pubnub.dataSync.removeChannel({
    id: string,
    ifMatchesEtag: string,
}): Promise<DataSync.RemoveChannelResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Channel identifier. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.removeChannel({ id: 'channel-summer-sale' })
    console.log(response.status)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200
}
```

## Memberships

A membership links a user to a channel and carries its own `payload`. In the running example, Alice is a member of `channel-summer-sale` with the payload `{ "role": "viewer" }`. Refer to [memberships](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#memberships) for the concept.

A membership is a relationship of the built-in Global `Membership` [relationship class](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md), whose two sides are surfaced as `channelId` and `userId`. Every membership response includes both `relationshipClass` and `relationshipClassVersion`. The class is assigned by the service, so the `createMembership` and `setMembership` methods take no class parameter.

Relationship classes [don't support inheritance](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#relationship-class-inheritance), and only the Global `Membership` class produces memberships, so `relationshipClass` is always `Membership` and `relationshipClassVersion` is the only part that varies.

### Create membership

Creates a membership linking a user to a channel. The `userId`/`channelId` pair must be unique for the membership class. A membership that duplicates an existing pair is rejected with a `409`.

#### Method(s)

```javascript
pubnub.dataSync.createMembership({
    id: string,
    userId: string,
    channelId: string,
    data: { classVersion: number, status: string, payload: object },
}): Promise<DataSync.CreateMembershipResponse>
```

| Parameter | Description |
| --- | --- |
| `id`Type: stringDefault: server-generated | Membership identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `userId` *Type: stringDefault: n/a | Identifier of the user in the membership. |
| `channelId` *Type: stringDefault: n/a | Identifier of the channel in the membership. |
| `data` *Type: objectDefault: n/a | The mutable membership data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the membership class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.createMembership({
        userId: 'user-alice',
        channelId: 'channel-summer-sale',
        data: {
            classVersion: 1,
            payload: { role: 'viewer' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "viewer"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T09:10:00.000Z",
        "eTag": "MnOpQrStUvWxYz"
    }
}
```

### Get membership

Returns a single membership by `id`.

#### Method(s)

```javascript
pubnub.dataSync.getMembership({
    id: string,
}): Promise<DataSync.GetMembershipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Membership identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getMembership({
        id: 'membership-alice-summer-sale',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "viewer"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T09:10:00.000Z",
        "eTag": "MnOpQrStUvWxYz"
    }
}
```

### Get all memberships

Returns a paginated list of memberships. All parameters are optional, so you can call `getMemberships()` with no arguments, or pass a callback as the only argument. Filter by `userId` to list a user's memberships or by `channelId` to list a channel's members. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```javascript
pubnub.dataSync.getMemberships({
    cursor: string,
    limit: number,
    filter: string,
    filterFast: string,
    sort: object | string,
    userId: string,
    channelId: string,
    classVersion: number,
}): Promise<DataSync.GetMembershipsResponse>
```

| Parameter | Description |
| --- | --- |
| `cursor`Type: stringDefault: n/a | Opaque pagination cursor. Omit for the first page. |
| `limit`Type: numberDefault: `20` | Maximum number of memberships per page. Max `100`. |
| `filter`Type: stringDefault: n/a | Filter expression evaluated against eventually consistent storage, so results can briefly lag writes. Supports the full expression language, over properties declared with filtering mode `full`. Cannot be combined with `filterFast`. |
| `filterFast`Type: stringDefault: n/a | Filter expression evaluated against strongly consistent storage, so it reflects the latest writes. Accepts up to 10 conditions by default (raisable per keyset), over properties declared with filtering mode `simple` or `full`. Cannot be combined with `filter`. |
| `sort`Type: object | stringDefault: n/a | Order results. An object mapping field to `'asc'`, `'desc'`, or `null` (service default), or a comma-separated string such as `'createdAt:desc'`. |
| `userId`Type: stringDefault: n/a | List only memberships for this user. |
| `channelId`Type: stringDefault: n/a | List only memberships for this channel. |
| `classVersion`Type: numberDefault: all versions | Membership class version to list. Omit to list memberships across **every** version of the class. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getMemberships({
        userId: 'user-alice',
        limit: 20,
    })
    console.log(response.data)
    console.log(response.meta)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "membership-alice-summer-sale",
            "channelId": "channel-summer-sale",
            "userId": "user-alice",
            "relationshipClass": "Membership",
            "relationshipClassVersion": 1,
            "payload": {
                "role": "viewer"
            },
            "createdAt": "2026-07-13T09:10:00.000Z",
            "updatedAt": "2026-07-13T09:10:00.000Z",
            "eTag": "MnOpQrStUvWxYz"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### List a channel's members with channelId

Pass `channelId` instead of `userId` to list the members of a channel rather than a user's memberships.

```javascript
try {
    const response = await pubnub.dataSync.getMemberships({
        channelId: 'channel-summer-sale',
        limit: 20,
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filter

`filter` runs over properties declared with filtering mode `full` and is evaluated against eventually consistent storage, so a very recent write may not be matched yet. Reference a declared property by its `name`, not its `path`.

```javascript
try {
    const response = await pubnub.dataSync.getMemberships({
        userId: 'user-alice',
        filter: 'role LIKE "*mod*"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filterFast

`filterFast` is evaluated against strongly consistent storage, so it matches an object you just wrote. It runs over properties declared with filtering mode `simple` or `full` and accepts up to 10 conditions by default (raisable per keyset). It shares the same expression language as `filter`, so only one of the two can be sent per call.

```javascript
try {
    const response = await pubnub.dataSync.getMemberships({
        channelId: 'channel-summer-sale',
        filterFast: 'role == "viewer"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `meta.next_cursor` from the response and pass it back as `cursor` on the next call. Stop when `meta.has_next` is `false`.

```javascript
try {
    let cursor
    let hasNext = true
    let page = 0

    while (hasNext) {
        const response = await pubnub.dataSync.getMemberships({
            userId: 'user-alice',
            limit: 20,
            cursor,
        })
        console.log(`Page ${++page}:`, response.data)
        cursor = response.meta?.next_cursor
        hasNext = response.meta?.has_next ?? false
    }
} catch (error) {
    console.log(error.status)
}
```

### Update membership

Replaces a membership in full (PUT). Resend `userId`, `channelId`, and `classVersion` along with the fields you want to keep. For a partial update, use [Patch membership](#patch-membership). Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`.

#### Method(s)

```javascript
pubnub.dataSync.setMembership({
    id: string,
    userId: string,
    channelId: string,
    data: { classVersion: number, status: string, payload: object },
    ifMatchesEtag: string,
}): Promise<DataSync.SetMembershipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Membership identifier. |
| `userId` *Type: stringDefault: n/a | Identifier of the user in the membership. |
| `channelId` *Type: stringDefault: n/a | Identifier of the channel in the membership. |
| `data` *Type: objectDefault: n/a | The replacement membership data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the membership class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.setMembership({
        id: 'membership-alice-summer-sale',
        userId: 'user-alice',
        channelId: 'channel-summer-sale',
        data: {
            classVersion: 1,
            payload: { role: 'moderator' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "moderator"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T12:00:00.000Z",
        "eTag": "OpQrStUvWxYzAb"
    }
}
```

### Patch membership

Applies a partial update to a membership. Paths can target fields inside `payload` or top-level stored fields. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the JSON Pointer rules.

#### Method(s)

```javascript
pubnub.dataSync.updateMembership({
    id: string,
    add: object,
    replace: object,
    remove: string[],
    move: { from: string, path: string }[],
    copy: { from: string, path: string }[],
    test: object,
    ifMatchesEtag: string,
}): Promise<DataSync.UpdateMembershipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Membership identifier. |
| `add`Type: objectDefault: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
| `replace`Type: objectDefault: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
| `remove`Type: arrayDefault: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
| `move`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is removed and re-added at `path`. |
| `copy`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is duplicated to `path`. |
| `test`Type: objectDefault: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

Use a JSON Pointer that starts with `/payload/` to target payload fields, or a root pointer (for example `/status`) to target a top-level field. At least one of `add`, `replace`, `remove`, `move`, `copy`, or `test` must be provided.

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.updateMembership({
        id: 'membership-alice-summer-sale',
        replace: { '/payload/role': 'moderator' },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "moderator"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T12:05:00.000Z",
        "eTag": "QrStUvWxYzAbCd"
    }
}
```

### Remove membership

Deletes a membership by `id`.

#### Method(s)

```javascript
pubnub.dataSync.removeMembership({
    id: string,
    ifMatchesEtag: string,
}): Promise<DataSync.RemoveMembershipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Membership identifier. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.removeMembership({
        id: 'membership-alice-summer-sale',
    })
    console.log(response.status)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200
}
```

## Entities

Entities are instances of the custom [entity classes](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md) you define on your key set. In the running example, `product` is a class and `product-sneaker-42` is an instance. Use [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) to declare a class, and [Get entity class by ID](https://www.pubnub.com/docs/admin-api/get-entity-class-by-id.md) to read the property, filtering, and projection declarations that govern its instances.

Entity classes can extend one another. Listing a class also returns the entities of its subclasses, so `getEntities({ class: 'product' })` returns every `product` plus every instance of a class that extends `product`.

### Create entity

Creates an entity of a given class and version.

#### Method(s)

```javascript
pubnub.dataSync.createEntity({
    id: string,
    class: string,
    classLevel: string,
    data: { classVersion: number, status: string, payload: object },
}): Promise<DataSync.CreateEntityResponse>
```

| Parameter | Description |
| --- | --- |
| `id`Type: stringDefault: server-generated | Entity identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `class` *Type: stringDefault: n/a | Name of the entity class this instance belongs to. Set at creation and immutable afterward. |
| `classLevel`Type: stringDefault: service default | Class hierarchy level of `class`, either `Global` for a class the service provides or `SubKey` for one defined on your key set. Set at creation and immutable afterward. |
| `data` *Type: objectDefault: n/a | The mutable entity data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the entity class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.createEntity({
        id: 'product-sneaker-42',
        class: 'product',
        data: {
            classVersion: 1,
            payload: { name: 'Retro Sneaker', price: 89.99, stock: 12 },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "payload": {
            "name": "Retro Sneaker",
            "price": 89.99,
            "stock": 12
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T09:15:00.000Z",
        "eTag": "QrStUvWxYzAbCd"
    }
}
```

### Get entity

Returns a single entity by `id`.

#### Method(s)

```javascript
pubnub.dataSync.getEntity({
    id: string,
}): Promise<DataSync.GetEntityResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Entity identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getEntity({ id: 'product-sneaker-42' })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "payload": {
            "name": "Retro Sneaker",
            "price": 89.99,
            "stock": 12
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T09:15:00.000Z",
        "eTag": "QrStUvWxYzAbCd"
    }
}
```

### Get all entities

Returns a paginated list of entities within a class. The `class` parameter is required, entities are always listed within the context of their class. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```javascript
pubnub.dataSync.getEntities({
    class: string,
    classVersion: number,
    classLevel: string,
    cursor: string,
    limit: number,
    filter: string,
    filterFast: string,
    sort: object | string,
}): Promise<DataSync.GetEntitiesResponse>
```

| Parameter | Description |
| --- | --- |
| `class` *Type: stringDefault: n/a | Name of the entity class to list. This parameter is required. |
| `classVersion`Type: numberDefault: all versions | Entity class version to list. Omit to list entities across **every** version of the class. |
| `classLevel`Type: stringDefault: service default | Class hierarchy level of `class`, either `Global` for a class the service provides or `SubKey` for one defined on your key set. Used to disambiguate a class name defined at both levels. |
| `cursor`Type: stringDefault: n/a | Opaque pagination cursor. Omit for the first page. |
| `limit`Type: numberDefault: `20` | Maximum number of entities per page. Max `100`. |
| `filter`Type: stringDefault: n/a | Filter expression evaluated against eventually consistent storage, so results can briefly lag writes. Supports the full expression language, over properties declared with filtering mode `full`. Cannot be combined with `filterFast`. |
| `filterFast`Type: stringDefault: n/a | Filter expression evaluated against strongly consistent storage, so it reflects the latest writes. Accepts up to 10 conditions by default (raisable per keyset), over properties declared with filtering mode `simple` or `full`. Cannot be combined with `filter`. |
| `sort`Type: object | stringDefault: n/a | Order results. An object mapping field to `'asc'`, `'desc'`, or `null` (service default), or a comma-separated string such as `'createdAt:desc'`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getEntities({
        class: 'product',
        sort: { price: 'desc' },
        limit: 20,
    })
    console.log(response.data)
    console.log(response.meta)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "product-sneaker-42",
            "entityClass": "product",
            "entityClassVersion": 1,
            "payload": {
                "name": "Retro Sneaker",
                "price": 89.99,
                "stock": 12
            },
            "createdAt": "2026-07-13T09:15:00.000Z",
            "updatedAt": "2026-07-13T09:15:00.000Z",
            "eTag": "QrStUvWxYzAbCd"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### Filter with filter

`filter` runs over properties declared with filtering mode `full` and is evaluated against eventually consistent storage, so a very recent write may not be matched yet. Reference a declared property by its `name` (not its `path`), and combine conditions with `&&` and `||`.

```javascript
try {
    const response = await pubnub.dataSync.getEntities({
        class: 'product',
        filter: 'name LIKE "*sneaker*" && !(status == "discontinued")',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filterFast

`filterFast` is evaluated against strongly consistent storage, so it matches an object you just wrote. It runs over properties declared with filtering mode `simple` or `full` and accepts up to 10 conditions by default (raisable per keyset). It shares the same expression language as `filter`, so only one of the two can be sent per call.

```javascript
try {
    const response = await pubnub.dataSync.getEntities({
        class: 'product',
        filterFast: 'price < 100 && stock > 0',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `meta.next_cursor` from the response and pass it back as `cursor` on the next call. Stop when `meta.has_next` is `false`.

```javascript
try {
    let cursor
    let hasNext = true
    let page = 0

    while (hasNext) {
        const response = await pubnub.dataSync.getEntities({
            class: 'product',
            filterFast: 'price < 100',
            limit: 20,
            cursor,
        })
        console.log(`Page ${++page}:`, response.data)
        cursor = response.meta?.next_cursor
        hasNext = response.meta?.has_next ?? false
    }
} catch (error) {
    console.log(error.status)
}
```

### Update entity

Replaces an entity in full (PUT). The `entityClass` is immutable after creation and cannot be sent. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`.

#### Method(s)

```javascript
pubnub.dataSync.setEntity({
    id: string,
    data: { classVersion: number, status: string, payload: object },
    ifMatchesEtag: string,
}): Promise<DataSync.SetEntityResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Entity identifier. |
| `data` *Type: objectDefault: n/a | The replacement entity data. The entity `class` is immutable and cannot be included. |
| `> classVersion` *Type: numberDefault: n/a | Version of the entity class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.setEntity({
        id: 'product-sneaker-42',
        data: {
            classVersion: 1,
            payload: { name: 'Retro Sneaker', price: 79.99, stock: 8 },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "payload": {
            "name": "Retro Sneaker",
            "price": 79.99,
            "stock": 8
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T13:00:00.000Z",
        "eTag": "StUvWxYzAbCdEf"
    }
}
```

### Patch entity

Applies a partial update to an entity. Paths can target fields inside `payload` or top-level stored fields. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the JSON Pointer rules.

#### Method(s)

```javascript
pubnub.dataSync.updateEntity({
    id: string,
    add: object,
    replace: object,
    remove: string[],
    move: { from: string, path: string }[],
    copy: { from: string, path: string }[],
    test: object,
    ifMatchesEtag: string,
}): Promise<DataSync.UpdateEntityResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Entity identifier. |
| `add`Type: objectDefault: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
| `replace`Type: objectDefault: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
| `remove`Type: arrayDefault: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
| `move`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is removed and re-added at `path`. |
| `copy`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is duplicated to `path`. |
| `test`Type: objectDefault: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

Use a JSON Pointer that starts with `/payload/` to target payload fields, or a root pointer (for example `/status`) to target a top-level field. At least one of `add`, `replace`, `remove`, `move`, `copy`, or `test` must be provided.

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.updateEntity({
        id: 'product-sneaker-42',
        replace: { '/payload/price': 79.99 },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "payload": {
            "name": "Retro Sneaker",
            "price": 79.99,
            "stock": 12
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T13:05:00.000Z",
        "eTag": "UvWxYzAbCdEfGh"
    }
}
```

#### Other examples

##### Combine patch operations, and guard the write with ifMatchesEtag

A single `updateEntity` call can mix `add`, `replace`, `remove`, `move`, `copy`, and `test`. The order you write the keys in does not matter, the SDK always sends the operations in the same order: `add`, `replace`, `remove`, `move`, `copy`, and `test` last. Add `ifMatchesEtag` (the `eTag` from a prior read) to reject the patch with a `412` if the entity changed since you read it, instead of silently overwriting a concurrent change.

```javascript
try {
    const response = await pubnub.dataSync.updateEntity({
        id: 'product-sneaker-42',
        test: { '/payload/stock': 8 },
        replace: { '/payload/price': 74.99 },
        add: { '/payload/tags/0': 'clearance' },
        remove: ['/payload/legacy/field'],
        move: [{ from: '/payload/legacyName', path: '/payload/displayName' }],
        copy: [{ from: '/payload/displayName', path: '/payload/previousName' }],
        ifMatchesEtag: 'StUvWxYzAbCdEf',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

### Remove entity

Deletes an entity by `id`.

#### Method(s)

```javascript
pubnub.dataSync.removeEntity({
    id: string,
    ifMatchesEtag: string,
}): Promise<DataSync.RemoveEntityResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Entity identifier. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.removeEntity({ id: 'product-sneaker-42' })
    console.log(response.status)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200
}
```

## Relationships

A relationship links two entities and carries its own `payload`. In the running example, the `ProductOwner` relationship links `seller-bob` to `product-sneaker-42`.

Relationships are instances of the [relationship classes](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md) you define on your key set. A relationship class declares the cardinality the service enforces (`one-to-one`, `one-to-many`, or `many-to-many`) and, optionally, which entity class each side must belong to. Use [Create a new relationship class](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md) to declare a class, and [Get relationship class by name and version](https://www.pubnub.com/docs/admin-api/get-relationship-class-by-name-and-version.md) to read it back.

### Create relationship

Creates a relationship between two entities. The relationship class's cardinality (one-to-one, one-to-many, or many-to-many) is enforced on create. A relationship that violates its class's cardinality is rejected with a `409`.

#### Method(s)

```javascript
pubnub.dataSync.createRelationship({
    id: string,
    class: string,
    entityAId: string,
    entityBId: string,
    data: { classVersion: number, status: string, payload: object },
}): Promise<DataSync.CreateRelationshipResponse>
```

| Parameter | Description |
| --- | --- |
| `id`Type: stringDefault: server-generated | Relationship identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `class` *Type: stringDefault: n/a | Name of the relationship class this instance belongs to. Set at creation and immutable afterward. |
| `entityAId` *Type: stringDefault: n/a | Identifier of the first linked entity. Immutable after creation. |
| `entityBId` *Type: stringDefault: n/a | Identifier of the second linked entity. Immutable after creation. |
| `data` *Type: objectDefault: n/a | The mutable relationship data. |
| `> classVersion` *Type: numberDefault: n/a | Version of the relationship class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.createRelationship({
        class: 'ProductOwner',
        entityAId: 'seller-bob',
        entityBId: 'product-sneaker-42',
        data: {
            classVersion: 1,
            payload: { since: '2026-07-13' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T09:20:00.000Z",
        "eTag": "WxYzAbCdEfGhIj"
    }
}
```

### Get relationship

Returns a single relationship by `id`.

#### Method(s)

```javascript
pubnub.dataSync.getRelationship({
    id: string,
}): Promise<DataSync.GetRelationshipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Relationship identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getRelationship({
        id: 'rel-bob-owns-sneaker-42',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T09:20:00.000Z",
        "eTag": "WxYzAbCdEfGhIj"
    }
}
```

### Get all relationships

Returns a paginated list of relationships within a class. The `class` parameter is required. Filter by `entityAId` or `entityBId` to list a specific entity's links. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```javascript
pubnub.dataSync.getRelationships({
    class: string,
    classVersion: number,
    entityAId: string,
    entityBId: string,
    cursor: string,
    limit: number,
    filter: string,
    filterFast: string,
    sort: object | string,
}): Promise<DataSync.GetRelationshipsResponse>
```

| Parameter | Description |
| --- | --- |
| `class` *Type: stringDefault: n/a | Name of the relationship class to list. This parameter is required. |
| `classVersion`Type: numberDefault: all versions | Relationship class version to list. Omit to list relationships across **every** version of the class. |
| `entityAId`Type: stringDefault: n/a | List only relationships whose first entity is this id. |
| `entityBId`Type: stringDefault: n/a | List only relationships whose second entity is this id. |
| `cursor`Type: stringDefault: n/a | Opaque pagination cursor. Omit for the first page. |
| `limit`Type: numberDefault: `20` | Maximum number of relationships per page. Max `100`. |
| `filter`Type: stringDefault: n/a | Filter expression evaluated against eventually consistent storage, so results can briefly lag writes. Supports the full expression language, over properties declared with filtering mode `full`. Cannot be combined with `filterFast`. |
| `filterFast`Type: stringDefault: n/a | Filter expression evaluated against strongly consistent storage, so it reflects the latest writes. Accepts up to 10 conditions by default (raisable per keyset), over properties declared with filtering mode `simple` or `full`. Cannot be combined with `filter`. |
| `sort`Type: object | stringDefault: n/a | Order results. An object mapping field to `'asc'`, `'desc'`, or `null` (service default), or a comma-separated string such as `'createdAt:desc'`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.getRelationships({
        class: 'ProductOwner',
        entityAId: 'seller-bob',
        limit: 20,
    })
    console.log(response.data)
    console.log(response.meta)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "rel-bob-owns-sneaker-42",
            "entityAId": "seller-bob",
            "entityBId": "product-sneaker-42",
            "relationshipClass": "ProductOwner",
            "relationshipClassVersion": 1,
            "payload": {
                "since": "2026-07-13"
            },
            "createdAt": "2026-07-13T09:20:00.000Z",
            "updatedAt": "2026-07-13T09:20:00.000Z",
            "eTag": "WxYzAbCdEfGhIj"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### List an entity's incoming links with entityBId

Pass `entityBId` instead of `entityAId` to list relationships where the entity is on the second side of the link.

```javascript
try {
    const response = await pubnub.dataSync.getRelationships({
        class: 'ProductOwner',
        entityBId: 'product-sneaker-42',
        limit: 20,
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filter

`filter` runs over properties declared with filtering mode `full` and is evaluated against eventually consistent storage, so a very recent write may not be matched yet. Reference a declared property by its `name`, not its `path`.

```javascript
try {
    const response = await pubnub.dataSync.getRelationships({
        class: 'ProductOwner',
        filter: '!(tier == "platinum")',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Filter with filterFast

`filterFast` is evaluated against strongly consistent storage, so it matches an object you just wrote. It runs over properties declared with filtering mode `simple` or `full` and accepts up to 10 conditions by default (raisable per keyset). It shares the same expression language as `filter`, so only one of the two can be sent per call.

```javascript
try {
    const response = await pubnub.dataSync.getRelationships({
        class: 'ProductOwner',
        filterFast: 'tier == "gold"',
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `meta.next_cursor` from the response and pass it back as `cursor` on the next call. Stop when `meta.has_next` is `false`.

```javascript
try {
    let cursor
    let hasNext = true
    let page = 0

    while (hasNext) {
        const response = await pubnub.dataSync.getRelationships({
            class: 'ProductOwner',
            entityAId: 'seller-bob',
            limit: 20,
            cursor,
        })
        console.log(`Page ${++page}:`, response.data)
        cursor = response.meta?.next_cursor
        hasNext = response.meta?.has_next ?? false
    }
} catch (error) {
    console.log(error.status)
}
```

### Update relationship

Replaces a relationship in full (PUT). The linked entity ids are immutable, so resend the same `entityAId` and `entityBId`. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`.

#### Method(s)

```javascript
pubnub.dataSync.setRelationship({
    id: string,
    entityAId: string,
    entityBId: string,
    data: { classVersion: number, status: string, payload: object },
    ifMatchesEtag: string,
}): Promise<DataSync.SetRelationshipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Relationship identifier. |
| `entityAId` *Type: stringDefault: n/a | Identifier of the first linked entity. Must match the value set at creation. |
| `entityBId` *Type: stringDefault: n/a | Identifier of the second linked entity. Must match the value set at creation. |
| `data` *Type: objectDefault: n/a | The replacement relationship data. The relationship `class` is immutable and cannot be included. |
| `> classVersion` *Type: numberDefault: n/a | Version of the relationship class schema. |
| `> status`Type: stringDefault: n/a | Free-form lifecycle status. Max 100 characters. |
| `> payload`Type: objectDefault: n/a | Free-form JSON object holding your application data. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.setRelationship({
        id: 'rel-bob-owns-sneaker-42',
        entityAId: 'seller-bob',
        entityBId: 'product-sneaker-42',
        data: {
            classVersion: 1,
            payload: { since: '2026-07-13', tier: 'gold' },
        },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13",
            "tier": "gold"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T14:00:00.000Z",
        "eTag": "YzAbCdEfGhIjKl"
    }
}
```

### Patch relationship

Applies a partial update to a relationship. Paths can target fields inside `payload` or top-level stored fields. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the JSON Pointer rules.

#### Method(s)

```javascript
pubnub.dataSync.updateRelationship({
    id: string,
    add: object,
    replace: object,
    remove: string[],
    move: { from: string, path: string }[],
    copy: { from: string, path: string }[],
    test: object,
    ifMatchesEtag: string,
}): Promise<DataSync.UpdateRelationshipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Relationship identifier. |
| `add`Type: objectDefault: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
| `replace`Type: objectDefault: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
| `remove`Type: arrayDefault: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
| `move`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is removed and re-added at `path`. |
| `copy`Type: arrayDefault: n/a | Array of `{ from, path }` JSON Pointer pairs. The value at `from` is duplicated to `path`. |
| `test`Type: objectDefault: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

Use a JSON Pointer that starts with `/payload/` to target payload fields, or a root pointer (for example `/status`) to target a top-level field. At least one of `add`, `replace`, `remove`, `move`, `copy`, or `test` must be provided.

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.updateRelationship({
        id: 'rel-bob-owns-sneaker-42',
        replace: { '/payload/tier': 'platinum' },
    })
    console.log(response.data)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13",
            "tier": "platinum"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T14:05:00.000Z",
        "eTag": "AbCdEfGhIjKlMn"
    }
}
```

### Remove relationship

Deletes a relationship by `id`.

#### Method(s)

```javascript
pubnub.dataSync.removeRelationship({
    id: string,
    ifMatchesEtag: string,
}): Promise<DataSync.RemoveRelationshipResponse>
```

| Parameter | Description |
| --- | --- |
| `id` *Type: stringDefault: n/a | Relationship identifier. |
| `ifMatchesEtag`Type: stringDefault: n/a | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```javascript
try {
    const response = await pubnub.dataSync.removeRelationship({
        id: 'rel-bob-owns-sneaker-42',
    })
    console.log(response.status)
} catch (error) {
    console.log(error.status)
}
```

#### Response

```json
{
    "status": 200
}
```

## Real-time updates

DataSync objects can publish `create`, `update`, and `delete` events that you receive in real time with a `dataSync` listener. Create a [DataSync SDK entity](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe.md#datasync-sdk-entities) for the object, subscribe to it, and attach the listener, described in the [Add DataSync listener](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe.md#add-datasync-listener) section of Publish and subscribe.

```javascript
const subscription = pubnub.dataSyncEntity('product-sneaker-42').subscription()
subscription.onDataSync = (event) => console.log(event.message.event, event.message.data)
subscription.subscribe()
```

Each event names the change in `event`, identifies the object kind in `objectType` (`user`, `channel`, `membership`, `entity`, or `relationship`), and carries the object state in `data`. For a `delete` event, `data` holds only the `id` and, when the service reports it, `deletedAt`.

### Where each event is delivered

A relationship or membership change is never delivered on a channel named after the relationship or membership id, only on the ids of the two entities it links. A `create` for a user, channel, or entity is delivered only on its own `id`. An `update` or `delete` for a user, channel, or entity is also delivered on the `id` of every entity, user, or channel connected to it by a relationship or membership, in either direction, at the time of the change:

| Change to | `create` delivered on | `update` / `delete` delivered on |
| --- | --- | --- |
| A user | The user's `id` | The user's `id`, plus the `id` of every entity, user, or channel connected to it by a relationship or membership |
| A channel | The channel's `id` | The channel's `id`, plus the `id` of every entity, user, or channel connected to it by a relationship or membership |
| An entity | The entity's `id` | The entity's `id`, plus the `id` of every entity, user, or channel connected to it by a relationship or membership |
| A membership | **Both** the `userId` and the `channelId` of the membership | **Both** the `userId` and the `channelId` of the membership |
| A relationship | **Both** the `entityAId` and the `entityBId` of the relationship | **Both** the `entityAId` and the `entityBId` of the relationship |

So to see memberships appear and disappear for Alice, observe `user-alice` rather than the membership id. Unlike entity/user/channel events, a membership or relationship `create` is delivered on both linked ids too, the same as its `update`/`delete`. A client observing both sides of the same link receives the change twice, once per channel. Deduplicate create and update events on `data.id` and `data.updatedAt`, and delete events on `data.id` and `data.deletedAt`.

Events are off by default and are enabled per class in the [Admin Portal](https://admin.pubnub.com). Refer to [enabling events](https://www.pubnub.com/docs/general/data-sync/events.md#enable-datasync-events) for how to turn them on, and [receiving events](https://www.pubnub.com/docs/general/data-sync/events.md#receive-datasync-events) for the listener flow.

Last updated at: 2026-09-08T17:09:13.000Z
