DataSync API for JavaScript SDK
DataSync 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.
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 in Publish and subscribe.
The two meet in the 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, not through this SDK:
- Entity classes, which back users, channels, and custom entities: list, read, create, replace, and delete. Partial updates aren't implemented; replace the complete class version instead.
- Relationship classes, which back memberships and custom relationships: list, read, create, replace, and delete.
A class definition is what decides, for every object of that class, which payload fields are filterable and sortable, which projection each field belongs to, and how long the object lives before it expires. Refer to 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
- Pagination
- Filtering and sorting
- Concurrency (ETag)
- Partial updates
- Expiry (TTL)
Every DataSync request must be authorized 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. Refer to error codes for the full list.
A signature means the client is configured with a secretKey, and the SDK then signs every request with it automatically. Never put a secret key in a client-side application, use it only on a server you control. Give client-side applications a token from grantToken() instead, set with setToken().
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 for details.
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 (configurable 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.
danger
filter is not the strongly consistent oneThe 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:
1filterFast: 'price < 100' // strongly consistent, single condition
2filterFast: '(price < 100 && stock > 0) || price > 500' // strongly consistent, grouped conditions
3filter: 'name LIKE "*sneaker*"' // eventually consistent, needs filtering mode "full"
4filter: '!(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. When you don't pass classVersion, the list includes items of every version, but filter, filterFast, and sort can only use properties declared on the latest version of the class. A property that exists only in an older version is rejected with a 400. Pass classVersion to use properties declared on an older version. Refer to filtering for the two filtering tiers, and 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:
1sort: { price: 'desc' } // single field, descending
2sort: { type: 'asc', price: 'desc' } // type first, then price within each type
3sort: { 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 :desc. A bare name sorts ascending:
1sort: 'price:desc' // single field, descending
2sort: 'type,price:desc' // type ascending, then price descending
3sort: 'createdAt' // bare name, 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.
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 for details.
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 for details.
Correct patch forms:
1add: { '/payload/tags/0': 'featured' } // adds a value inside payload.tags
2replace: { '/payload/price': 79.99 } // replaces payload.price
3remove: ['/payload/tempFlag', '/payload/legacy/field'] // removes payload.tempFlag and payload.legacy.field
4move: [{ from: '/payload/legacyName', path: '/payload/displayName' }]
5copy: [{ from: '/payload/displayName', path: '/payload/previousName' }]
6test: { '/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.
An object's expiresAt is set once when the object is created, from its class TTL, and updates never refresh it. Refer to data expiry for the default TTLs, the day rounding, and how relationships expire.
Supported and recommended asynchronous patterns
PubNub supports Callbacks, Promises, and Async/Await 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 syntax to your code.
Requires Access Manager
DataSync requires that the Access Manager add-on is enabled for your key in the Admin Portal. Read the support page 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 for the concept.
A user is an entity of the built-in User entity class, 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 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)
1pubnub.dataSync.createUser({
2 id: string,
3 class: string,
4 classLevel: string,
5 data: { classVersion: number, status: string, payload: object },
6}): Promise<DataSync.CreateUserResponse>
| Parameter | Description |
|---|---|
idType: string Default: server-generated | User identifier. Omit to let the server generate a UUID. Max 255 characters. |
classType: string Default: User | Name of the entity class this user belongs to. Must be User or one of its subclasses. Set at creation and immutable afterward. |
classLevelType: string Default: 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: object Default: n/a | The mutable user data. |
> classVersion *Type: number Default: n/a | Version of the user class schema. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: object Default: n/a | Free-form JSON object holding your application data. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "entityClassVersion": 1,
6 "payload": {
7 "name": "Alice",
8 "type": "shopper"
9 },
10 "createdAt": "2026-07-13T09:00:00.000Z",
11 "updatedAt": "2026-07-13T09:00:00.000Z",
12 "eTag": "AbQdEfGhIjKlMn"
13 }
14}
Get user
Returns a single user by id.
Method(s)
1pubnub.dataSync.getUser({
2 id: string,
3}): Promise<DataSync.GetUserResponse>
| Parameter | Description |
|---|---|
id *Type: string Default: n/a | User identifier. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "entityClassVersion": 1,
6 "payload": {
7 "name": "Alice",
8 "type": "shopper"
9 },
10 "createdAt": "2026-07-13T09:00:00.000Z",
11 "updatedAt": "2026-07-13T09:00:00.000Z",
12 "eTag": "AbQdEfGhIjKlMn"
13 }
14}
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.
Method(s)
1pubnub.dataSync.getUsers({
2 class: string,
3 classVersion: number,
4 classLevel: string,
5 cursor: string,
6 limit: number,
7 filter: string,
8 filterFast: string,
9 sort: object | string,
10}): Promise<DataSync.GetUsersResponse>
| Parameter | Description |
|---|---|
classType: string Default: all user classes | Entity class name to filter by. Omit to list users across every user class. |
classVersionType: number Default: all versions | Entity class version to list. Omit to list users across every version of the class. |
classLevelType: string Default: 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. |
cursorType: string Default: n/a | Opaque pagination cursor. Omit for the first page. |
limitType: number Default: 20 | Maximum number of users per page. Max 100. |
filterType: string Default: 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. |
filterFastType: string Default: 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. |
sortType: object | string Default: 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
Reference code
1
Response
1{
2 "status": 200,
3 "data": [
4 {
5 "id": "user-alice",
6 "entityClassVersion": 1,
7 "payload": {
8 "name": "Alice",
9 "type": "shopper"
10 },
11 "createdAt": "2026-07-13T09:00:00.000Z",
12 "updatedAt": "2026-07-13T09:00:00.000Z",
13 "eTag": "AbQdEfGhIjKlMn"
14 }
15 ],
show all 21 linesOther 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.
1
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.
1
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.
1
Set 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).
Method(s)
1pubnub.dataSync.setUser({
2 id: string,
3 data: { classVersion: number, status: string, payload: object },
4 ifMatchesEtag: string,
5}): Promise<DataSync.SetUserResponse>
| Parameter | Description |
|---|---|
id *Type: string Default: n/a | User identifier. |
data *Type: object Default: n/a | The replacement user data. |
> classVersion *Type: number Default: n/a | Version of the user class schema. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: object Default: n/a | Free-form JSON object holding your application data. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The update succeeds only if it still matches, otherwise the server returns 412. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "entityClassVersion": 1,
6 "payload": {
7 "name": "Alice B.",
8 "type": "shopper"
9 },
10 "createdAt": "2026-07-13T09:00:00.000Z",
11 "updatedAt": "2026-07-13T10:15:00.000Z",
12 "eTag": "CdEfGhIjKlMnOp"
13 }
14}
Update user
Applies a partial update to a user. Paths can target fields inside payload or top-level stored fields. Refer to partial update for the JSON Pointer rules.
Method(s)
1pubnub.dataSync.updateUser({
2 id: string,
3 add: object,
4 replace: object,
5 remove: string[],
6 move: { from: string, path: string }[],
7 copy: { from: string, path: string }[],
8 test: object,
9 ifMatchesEtag: string,
10}): Promise<DataSync.UpdateUserResponse>
| Parameter | Description |
|---|---|
id *Type: string Default: n/a | User identifier. |
addType: object Default: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
replaceType: object Default: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
removeType: array Default: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
moveType: array Default: n/a | Array of { from, path } JSON Pointer pairs. The value at from is removed and re-added at path. |
copyType: array Default: n/a | Array of { from, path } JSON Pointer pairs. The value at from is duplicated to path. |
testType: object Default: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. |
ifMatchesEtagType: string Default: 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
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "entityClassVersion": 1,
6 "status": "active",
7 "payload": {
8 "name": "Alice B.",
9 "type": "shopper"
10 },
11 "createdAt": "2026-07-13T09:00:00.000Z",
12 "updatedAt": "2026-07-13T10:20:00.000Z",
13 "eTag": "EfGhIjKlMnOpQr"
14 }
15}
Remove user
Deletes a user by id.
Method(s)
1pubnub.dataSync.removeUser({
2 id: string,
3 ifMatchesEtag: string,
4}): Promise<DataSync.RemoveUserResponse>
| Parameter | Description |
|---|---|
id *Type: string Default: n/a | User identifier. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The delete succeeds only if it still matches, otherwise the server returns 412. |
Sample code
Reference code
1