DataSync API for Dart 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.
The Dart SDK has no DataSync SDK entities that carry stored state, but it does provide real-time handles for subscribing. pubnub.dataSyncUser(), pubnub.dataSyncChannel(), and pubnub.dataSyncEntity() take an object id and return a handle whose subscription() method builds the data channel name for you. Listen on the dataSync stream of the returned Subscription, as described in 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.
User and Channel entity classes and the Membership relationship class are predefined on every keyset, so you only need to define classes for your own custom entities and relationships.
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 method returns a Future of a typed result. The single-object results hold the record in a named getter: user, channel, membership, entity, or relationship. The list results hold a list of records in users, channels, memberships, entities, or relationships, describe the pagination in page, nextCursor, and hasNext, and can carry the HATEOAS links (self, and next where it applies) the service adds to a list response. The removeUser, removeChannel, removeMembership, removeEntity, and removeRelationship methods return a result that carries no data. The status key in each Response block below stands for the HTTP status code, which the result doesn't expose, a failed request throws instead. A stored object carries its free-form status and its expiresAt auto-deletion timestamp (ISO 8601) whenever those are set on it, and the timestamps stay ISO 8601 strings rather than becoming DateTime objects.
- 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.
A client that holds a secret key signs its requests. A client without one calls pubnub.setToken with a token from Grant token first.
DataSync pagination is forward-only. There is no previous-page cursor, and DataSyncPage exposes no previous cursor or hasPrev. 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 DataSyncPage in page with hasNext, nextCursor, and limit. The page is optional, so guard the access when you read it, for example result.page?.nextCursor. The result also offers nextCursor and hasNext directly, and hasNext is false when the service sends no page.
To page forward, pass the returned nextCursor back as cursor on the next call, and stop when hasNext is false. nextCursor is null on the last page. Refer to sorting and pagination for details.
All list methods accept two mutually exclusive filter parameters, filterFast and filter. 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.
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.
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, and null is only valid with == and !=. 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 only work over properties declared on the class, plus the built-in id, createdAt, updatedAt, and status fields, which are always filterable and sortable without being declared. eTag and expiresAt are neither. The status exception does not hold if the class redeclares /status scoped to projections your token cannot fully reach. Refer to filtering for the two filtering tiers, and property definitions for how properties are declared.
Use sort to order results. Its value is a comma-separated list of fields, each a property name optionally suffixed with :asc or :desc. A bare name sorts ascending:
1sort: 'price:desc', // single field, descending
2sort: 'type,price:desc', // type ascending, then price descending
3sort: 'createdAt:asc', // explicit ascending
The +field and -field prefixes are not accepted. A leading - is read as part of the property name, so '-price' fails with a 400. 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, updatedAt, and status are always sortable without declaring them on the class.
The SDK doesn't validate filterFast, filter, sort, or limit locally, it passes them through as-is. An invalid expression, an unsortable field, or an out-of-range limit only surfaces as an error response from the server, not as a client-side exception. The one thing the SDK does is drop an empty filterFast, filter, sort, or cursor string instead of sending it.
Every stored object carries an eTag. To guard against concurrent writes, pass the eTag you read earlier as ifMatchesEtag on set*, update*, and remove* calls. 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 412 arrives as a DataSyncException whose statusCode is 412.
The update* methods apply a partial update using JSON Patch (RFC 6902). They accept add, replace, and test (JSON Pointer to value maps), remove (a list of JSON Pointers), and move and copy (lists of JsonPointerPair, each with a from and a path).
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 /payload/price, not /price. A bare /price targets a top-level field named price, which doesn't exist. Top-level fields like /status can be patched directly. Refer to partial update for details.
Correct patch forms:
1add: {'/payload/tags': ['sale']}, // adds a value inside payload.tags
2replace: {'/payload/price': 79.99}, // replaces payload.price
3remove: ['/payload/tempFlag'], // removes payload.tempFlag
4move: [JsonPointerPair(from: '/payload/oldTag', path: '/payload/tag')],
5copy: [JsonPointerPair(from: '/payload/price', path: '/payload/msrp')],
6test: {'/status': 'active'}, // fails the patch if the current value doesn't match
move and copy take the source pointer in from and the target pointer in path, instead of a value. test fails the entire patch if the current value at its path doesn't match, use it to guard the rest of the operations against a stale read.
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.
All operations in the call apply together or not at all.
The order you write the arguments in does not matter, the SDK always sends the operations in the same order: add, replace, remove, move, copy, and test last. Keys and items keep their insertion order within each group.
At least one of add, replace, remove, move, copy, or test must be provided, and it must be non-empty. An empty map or list does not count. The SDK checks this before it sends anything and throws an InvariantException if nothing is provided.
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 for details.
Futures, and how errors surface
Every DataSync method is asynchronous and returns a Future, so await it. The samples do. An argument the SDK can check locally, such as an empty id, an empty class name, or a missing patch operation, throws an InvariantException before anything is sent. When the service rejects a request, the Future completes with a DataSyncException that carries the machine-readable errorCode (for example DS-0300), the human-readable errorMessage, the path of the offending input when there is one, and the HTTP statusCode. When the service reports several errors, errors lists all of them. Branch on errorCode, not on errorMessage, because the message can change between releases. An Access Manager denial without DataSync error details throws a ForbiddenException instead.
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 className.
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 UserInput user, {
3 Keyset? keyset,
4 String? using,
5}) // Future<CreateUserResult>
| Parameter | Description |
|---|---|
user *Type: UserInput Default: n/a | The user to create. |
> idType: string Default: server-generated | User identifier. Omit to let the server generate a UUID. Max 255 characters. |
> classNameType: string Default: User | Name of the entity class this user belongs to. Must be User or one of its subclasses. Defaults to User on the server if omitted. Set at creation and immutable afterward. Sent to the service as entityClass. |
> classVersion *Type: int Default: n/a | Version of the user class schema. Sent to the service as entityClassVersion. |
> classLevelType: ClassLevel Default: service default | Class hierarchy level of className, either ClassLevel.global for a class the service provides or ClassLevel.subKey for one defined on your keyset. Used to disambiguate classes with the same name defined at different levels. Set at creation and immutable afterward. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>Default: n/a | Free-form JSON object holding your application data. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 201,
3 "data": {
4 "id": "user-alice",
5 "entityClass": "User",
6 "entityClassVersion": 1,
7 "entityClassLevel": "Global",
8 "payload": { "name": "Alice", "type": "shopper" },
9 "createdAt": "2026-07-13T09:00:00.000Z",
10 "updatedAt": "2026-07-13T09:00:00.000Z",
11 "eTag": "AbCdEfGhIjKlMn"
12 }
13}
Get user
Returns a single user by userId.
Method(s)
1pubnub.dataSync.getUser(
2 String userId, {
3 Keyset? keyset,
4 String? using,
5}) // Future<GetUserResult>
| Parameter | Description |
|---|---|
userId *Type: string Default: n/a | User identifier. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "entityClass": "User",
6 "entityClassVersion": 1,
7 "entityClassLevel": "Global",
8 "payload": { "name": "Alice", "type": "shopper" },
9 "createdAt": "2026-07-13T09:00:00.000Z",
10 "updatedAt": "2026-07-13T09:00:00.000Z",
11 "eTag": "AbCdEfGhIjKlMn"
12 }
13}
Get all users
Returns a paginated list of users. All parameters are optional, so you can call getUsers with no arguments. For pagination, filtering, and sorting, refer to sorting and pagination.
Method(s)
1pubnub.dataSync.getUsers({
2 String? className,
3 int? classVersion,
4 ClassLevel? classLevel,
5 String? cursor,
6 int? limit,
7 String? filterFast,
8 String? filter,
9 String? sort,
10 Keyset? keyset,
11 String? using,
12}) // Future<GetUsersResult>
| Parameter | Description |
|---|---|
classNameType: string Default: all user classes | User class to list. Omit to list the Global User class and all of its subclasses. |
classVersionType: int Default: latest version | User class version to list. Omit to use the latest version of the class. |
classLevelType: ClassLevel Default: service default | Class hierarchy level of className, either ClassLevel.global for a class the service provides or ClassLevel.subKey for one defined on your keyset. Used to disambiguate a class name defined at both levels. |
cursorType: string Default: n/a | Opaque pagination cursor. Omit for the first page. |
limitType: int Default: 20 | Maximum number of users per page. Max 100. |
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. |
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. |
sortType: string Default: n/a | Order results. Pass a comma-separated list of fields each optionally suffixed with :asc or :desc, for example createdAt:desc. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": [
4 {
5 "id": "user-alice",
6 "entityClass": "User",
7 "entityClassVersion": 1,
8 "entityClassLevel": "Global",
9 "payload": { "name": "Alice", "type": "shopper" },
10 "createdAt": "2026-07-13T09:00:00.000Z",
11 "updatedAt": "2026-07-13T09:00:00.000Z",
12 "eTag": "AbCdEfGhIjKlMn"
13 }
14 ],
15 "meta": {
show all 20 linesOther examples
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). Reference a declared property by its name, not its path. It shares the same expression language as filter, so only one of the two can be sent per call.
1
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. It shares the same expression language as filterFast, 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 nextCursor from the result and pass it back as cursor on the next call. Stop when hasNext is false.
1
Set user
Replaces a user in full (PUT). Send the complete set of fields, any field you omit is cleared. The class is immutable after creation, so UserUpdate doesn't expose it and there's nothing to send. Refer to optimistic concurrency with ETags for ifMatchesEtag. For a partial update, use Update user.
Method(s)
1pubnub.dataSync.setUser(
2 String userId,
3 UserUpdate user, {
4 String? ifMatchesEtag,
5 Keyset? keyset,
6 String? using,
7 }
8) // Future<SetUserResult>
| Parameter | Description |
|---|---|
userId *Type: string Default: n/a | User identifier. |
user *Type: UserUpdate Default: n/a | The replacement user data. The entity class is immutable and cannot be included. |
> classVersion *Type: int Default: n/a | Version of the user class schema. Sent to the service as entityClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "entityClass": "User",
6 "entityClassVersion": 1,
7 "entityClassLevel": "Global",
8 "payload": { "name": "Alice B.", "type": "shopper" },
9 "createdAt": "2026-07-13T09:00:00.000Z",
10 "updatedAt": "2026-07-13T10:15:00.000Z",
11 "eTag": "CdEfGhIjKlMnOp"
12 }
13}
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 String userId, {
3 Map<String, dynamic>? add,
4 Map<String, dynamic>? replace,
5 List<String>? remove,
6 List<JsonPointerPair>? move,
7 List<JsonPointerPair>? copy,
8 Map<String, dynamic>? test,
9 String? ifMatchesEtag,
10 Keyset? keyset,
11 String? using,
12}) // Future<UpdateUserResult>
| Parameter | Description |
|---|---|
userId *Type: string Default: n/a | User identifier. |
addType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
replaceType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
removeType: List <String>Default: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
moveType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is removed and re-added at path. Provide at least one patch operation. |
copyType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is duplicated to path. Provide at least one patch operation. |
testType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. Provide at least one patch operation. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The patch succeeds only if it still matches, otherwise the server returns 412. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
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.
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "user-alice",
5 "status": "active",
6 "entityClass": "User",
7 "entityClassVersion": 1,
8 "entityClassLevel": "Global",
9 "payload": { "name": "Alice B.", "type": "shopper" },
10 "createdAt": "2026-07-13T09:00:00.000Z",
11 "updatedAt": "2026-07-13T10:20:00.000Z",
12 "eTag": "EfGhIjKlMnOpQr"
13 }
14}
Remove user
Deletes a user by userId.
Method(s)
1pubnub.dataSync.removeUser(
2 String userId, {
3 String? ifMatchesEtag,
4 Keyset? keyset,
5 String? using,
6}) // Future<RemoveUserResult>
| Parameter | Description |
|---|---|
userId *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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200
3}
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 for the concept.
A channel is an entity of the built-in Channel entity class, which the service provides at the Global class level. Subclass it with Create a new entity class to add declared properties, then pass the subclass name as className.
Create channel
Creates a channel. Supply an id to control the identifier, or omit it to let the server generate one.
Method(s)
1pubnub.dataSync.createChannel(
2 ChannelInput channel, {
3 Keyset? keyset,
4 String? using,
5}) // Future<CreateChannelResult>
| Parameter | Description |
|---|---|
channel *Type: ChannelInput Default: n/a | The channel to create. |
> idType: string Default: server-generated | Channel identifier. Omit to let the server generate a UUID. Max 255 characters. |
> classNameType: string Default: Channel | Name of the entity class this channel belongs to. Must be Channel or one of its subclasses. Defaults to Channel on the server if omitted. Set at creation and immutable afterward. Sent to the service as entityClass. |
> classVersion *Type: int Default: n/a | Version of the channel class schema. Sent to the service as entityClassVersion. |
> classLevelType: ClassLevel Default: service default | Class hierarchy level of className, either ClassLevel.global for a class the service provides or ClassLevel.subKey for one defined on your keyset. Used to disambiguate classes with the same name defined at different levels. Set at creation and immutable afterward. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>Default: n/a | Free-form JSON object holding your application data. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 201,
3 "data": {
4 "id": "channel-summer-sale",
5 "entityClass": "Channel",
6 "entityClassVersion": 1,
7 "entityClassLevel": "Global",
8 "payload": { "name": "Summer Sale", "type": "promotion" },
9 "createdAt": "2026-07-13T09:05:00.000Z",
10 "updatedAt": "2026-07-13T09:05:00.000Z",
11 "eTag": "GhIjKlMnOpQrSt"
12 }
13}
Get channel
Returns a single channel by channelId.
Method(s)
1pubnub.dataSync.getChannel(
2 String channelId, {
3 Keyset? keyset,
4 String? using,
5}) // Future<GetChannelResult>
| Parameter | Description |
|---|---|
channelId *Type: string Default: n/a | Channel identifier. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "channel-summer-sale",
5 "entityClass": "Channel",
6 "entityClassVersion": 1,
7 "entityClassLevel": "Global",
8 "payload": { "name": "Summer Sale", "type": "promotion" },
9 "createdAt": "2026-07-13T09:05:00.000Z",
10 "updatedAt": "2026-07-13T09:05:00.000Z",
11 "eTag": "GhIjKlMnOpQrSt"
12 }
13}
Get all channels
Returns a paginated list of channels. All parameters are optional, so you can call getChannels with no arguments. For pagination, filtering, and sorting, refer to sorting and pagination.
Method(s)
1pubnub.dataSync.getChannels({
2 String? className,
3 int? classVersion,
4 ClassLevel? classLevel,
5 String? cursor,
6 int? limit,
7 String? filterFast,
8 String? filter,
9 String? sort,
10 Keyset? keyset,
11 String? using,
12}) // Future<GetChannelsResult>
| Parameter | Description |
|---|---|
classNameType: string Default: all channel classes | Channel class to list. Omit to list the Global Channel class and all of its subclasses. |
classVersionType: int Default: latest version | Channel class version to list. Omit to use the latest version of the class. |
classLevelType: ClassLevel Default: service default | Class hierarchy level of className, either ClassLevel.global for a class the service provides or ClassLevel.subKey for one defined on your keyset. Used to disambiguate a class name defined at both levels. |
cursorType: string Default: n/a | Opaque pagination cursor. Omit for the first page. |
limitType: int Default: 20 | Maximum number of channels per page. Max 100. |
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. |
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. |
sortType: string Default: n/a | Order results. Pass a comma-separated list of fields each optionally suffixed with :asc or :desc. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": [
4 {
5 "id": "channel-summer-sale",
6 "entityClass": "Channel",
7 "entityClassVersion": 1,
8 "entityClassLevel": "Global",
9 "payload": { "name": "Summer Sale", "type": "promotion" },
10 "createdAt": "2026-07-13T09:05:00.000Z",
11 "updatedAt": "2026-07-13T09:05:00.000Z",
12 "eTag": "GhIjKlMnOpQrSt"
13 }
14 ],
15 "meta": {
show all 20 linesOther examples
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). Reference a declared property by its name, not its path. It shares the same expression language as filter, so only one of the two can be sent per call.
1
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. It shares the same expression language as filterFast, 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 nextCursor from the result and pass it back as cursor on the next call. Stop when hasNext is false.
1
Set channel
Replaces a channel in full (PUT). Send the complete set of fields, any field you omit is cleared. The class is immutable after creation, so ChannelUpdate doesn't expose it and there's nothing to send. Refer to optimistic concurrency with ETags for ifMatchesEtag. For a partial update, use Update channel.
Method(s)
1pubnub.dataSync.setChannel(
2 String channelId,
3 ChannelUpdate channel, {
4 String? ifMatchesEtag,
5 Keyset? keyset,
6 String? using,
7 }
8) // Future<SetChannelResult>
| Parameter | Description |
|---|---|
channelId *Type: string Default: n/a | Channel identifier. |
channel *Type: ChannelUpdate Default: n/a | The replacement channel data. The entity class is immutable and cannot be included. |
> classVersion *Type: int Default: n/a | Version of the channel class schema. Sent to the service as entityClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "channel-summer-sale",
5 "entityClass": "Channel",
6 "entityClassVersion": 1,
7 "entityClassLevel": "Global",
8 "payload": { "name": "Summer Sale 2026", "type": "promotion" },
9 "createdAt": "2026-07-13T09:05:00.000Z",
10 "updatedAt": "2026-07-13T11:00:00.000Z",
11 "eTag": "IjKlMnOpQrStUv"
12 }
13}
Update channel
Applies a partial update to a channel. Paths can target fields inside payload or top-level stored fields. Refer to partial update for the JSON Pointer rules.
Method(s)
1pubnub.dataSync.updateChannel(
2 String channelId, {
3 Map<String, dynamic>? add,
4 Map<String, dynamic>? replace,
5 List<String>? remove,
6 List<JsonPointerPair>? move,
7 List<JsonPointerPair>? copy,
8 Map<String, dynamic>? test,
9 String? ifMatchesEtag,
10 Keyset? keyset,
11 String? using,
12}) // Future<UpdateChannelResult>
| Parameter | Description |
|---|---|
channelId *Type: string Default: n/a | Channel identifier. |
addType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
replaceType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
removeType: List <String>Default: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
moveType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is removed and re-added at path. Provide at least one patch operation. |
copyType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is duplicated to path. Provide at least one patch operation. |
testType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. Provide at least one patch operation. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The patch succeeds only if it still matches, otherwise the server returns 412. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
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.
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "channel-summer-sale",
5 "status": "active",
6 "entityClass": "Channel",
7 "entityClassVersion": 1,
8 "entityClassLevel": "Global",
9 "payload": { "name": "Summer Sale 2026", "type": "promotion" },
10 "createdAt": "2026-07-13T09:05:00.000Z",
11 "updatedAt": "2026-07-13T11:05:00.000Z",
12 "eTag": "KlMnOpQrStUvWx"
13 }
14}
Remove channel
Deletes a channel by channelId.
Method(s)
1pubnub.dataSync.removeChannel(
2 String channelId, {
3 String? ifMatchesEtag,
4 Keyset? keyset,
5 String? using,
6}) // Future<RemoveChannelResult>
| Parameter | Description |
|---|---|
channelId *Type: string Default: n/a | Channel 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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200
3}
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 for the concept.
A membership is a relationship of the built-in Global Membership relationship class, 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, 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)
1pubnub.dataSync.createMembership(
2 MembershipInput membership, {
3 Keyset? keyset,
4 String? using,
5}) // Future<CreateMembershipResult>
| Parameter | Description |
|---|---|
membership *Type: MembershipInput Default: n/a | The membership to create. |
> idType: string Default: server-generated | Membership identifier. Omit to let the server generate a UUID. Max 255 characters. |
> channelId *Type: string Default: n/a | Identifier of the channel in the membership. |
> userId *Type: string Default: n/a | Identifier of the user in the membership. |
> classVersion *Type: int Default: n/a | Version of the membership class schema. Sent to the service as relationshipClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>Default: n/a | Free-form JSON object holding your application data. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 201,
3 "data": {
4 "id": "membership-alice-summer-sale",
5 "channelId": "channel-summer-sale",
6 "userId": "user-alice",
7 "relationshipClass": "Membership",
8 "relationshipClassVersion": 1,
9 "payload": { "role": "viewer" },
10 "createdAt": "2026-07-13T09:10:00.000Z",
11 "updatedAt": "2026-07-13T09:10:00.000Z",
12 "eTag": "MnOpQrStUvWxYz"
13 }
14}
Get membership
Returns a single membership by membershipId.
Method(s)
1pubnub.dataSync.getMembership(
2 String membershipId, {
3 Keyset? keyset,
4 String? using,
5}) // Future<GetMembershipResult>
| Parameter | Description |
|---|---|
membershipId *Type: string Default: n/a | Membership identifier. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "membership-alice-summer-sale",
5 "channelId": "channel-summer-sale",
6 "userId": "user-alice",
7 "relationshipClass": "Membership",
8 "relationshipClassVersion": 1,
9 "payload": { "role": "viewer" },
10 "createdAt": "2026-07-13T09:10:00.000Z",
11 "updatedAt": "2026-07-13T09:10:00.000Z",
12 "eTag": "MnOpQrStUvWxYz"
13 }
14}
Get all memberships
Returns a paginated list of memberships. All parameters are optional, so you can call getMemberships with no arguments. userId and channelId can be set independently, together, or omitted entirely. 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.
Method(s)
1pubnub.dataSync.getMemberships({
2 String? userId,
3 String? channelId,
4 int? classVersion,
5 String? cursor,
6 int? limit,
7 String? filterFast,
8 String? filter,
9 String? sort,
10 Keyset? keyset,
11 String? using,
12}) // Future<GetMembershipsResult>
| Parameter | Description |
|---|---|
userIdType: string Default: n/a | List only memberships for this user. |
channelIdType: string Default: n/a | List only memberships for this channel. |
classVersionType: int Default: latest version | Membership class version to list. Omit to use the latest version of the class. |
cursorType: string Default: n/a | Opaque pagination cursor. Omit for the first page. |
limitType: int Default: 20 | Maximum number of memberships per page. Max 100. |
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. |
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. |
sortType: string Default: n/a | Order results. Pass a comma-separated list of fields each optionally suffixed with :asc or :desc. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": [
4 {
5 "id": "membership-alice-summer-sale",
6 "channelId": "channel-summer-sale",
7 "userId": "user-alice",
8 "relationshipClass": "Membership",
9 "relationshipClassVersion": 1,
10 "payload": { "role": "viewer" },
11 "createdAt": "2026-07-13T09:10:00.000Z",
12 "updatedAt": "2026-07-13T09:10:00.000Z",
13 "eTag": "MnOpQrStUvWxYz"
14 }
15 ],
show all 21 linesOther 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.
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). Reference a declared property by its name, not its path. It shares the same expression language as filter, so only one of the two can be sent per call.
1
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. It shares the same expression language as filterFast, 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 nextCursor from the result and pass it back as cursor on the next call. Stop when hasNext is false.
1
Set membership
Replaces a membership in full (PUT). Send the complete set of fields, any field you omit is cleared. The channel and the user are immutable, so MembershipUpdate doesn't expose channelId or userId at all, there's nothing to resend. Refer to optimistic concurrency with ETags for ifMatchesEtag. For a partial update, use Update membership.
Method(s)
1pubnub.dataSync.setMembership(
2 String membershipId,
3 MembershipUpdate membership, {
4 String? ifMatchesEtag,
5 Keyset? keyset,
6 String? using,
7 }
8) // Future<SetMembershipResult>
| Parameter | Description |
|---|---|
membershipId *Type: string Default: n/a | Membership identifier. |
membership *Type: MembershipUpdate Default: n/a | The replacement membership data. The channel, the user, and the relationship class are immutable and cannot be included. |
> classVersion *Type: int Default: n/a | Version of the membership class schema. Sent to the service as relationshipClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "membership-alice-summer-sale",
5 "channelId": "channel-summer-sale",
6 "userId": "user-alice",
7 "relationshipClass": "Membership",
8 "relationshipClassVersion": 1,
9 "payload": { "role": "moderator" },
10 "createdAt": "2026-07-13T09:10:00.000Z",
11 "updatedAt": "2026-07-13T12:00:00.000Z",
12 "eTag": "OpQrStUvWxYzAb"
13 }
14}
Update membership
Applies a partial update to a membership. Paths can target fields inside payload or top-level stored fields. Refer to partial update for the JSON Pointer rules.
Method(s)
1pubnub.dataSync.updateMembership(
2 String membershipId, {
3 Map<String, dynamic>? add,
4 Map<String, dynamic>? replace,
5 List<String>? remove,
6 List<JsonPointerPair>? move,
7 List<JsonPointerPair>? copy,
8 Map<String, dynamic>? test,
9 String? ifMatchesEtag,
10 Keyset? keyset,
11 String? using,
12}) // Future<UpdateMembershipResult>
| Parameter | Description |
|---|---|
membershipId *Type: string Default: n/a | Membership identifier. |
addType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
replaceType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
removeType: List <String>Default: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
moveType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is removed and re-added at path. Provide at least one patch operation. |
copyType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is duplicated to path. Provide at least one patch operation. |
testType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. Provide at least one patch operation. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The patch succeeds only if it still matches, otherwise the server returns 412. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
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.
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "membership-alice-summer-sale",
5 "status": "active",
6 "channelId": "channel-summer-sale",
7 "userId": "user-alice",
8 "relationshipClass": "Membership",
9 "relationshipClassVersion": 1,
10 "payload": { "role": "moderator" },
11 "createdAt": "2026-07-13T09:10:00.000Z",
12 "updatedAt": "2026-07-13T12:05:00.000Z",
13 "eTag": "QrStUvWxYzAbCd"
14 }
15}
Remove membership
Deletes a membership by membershipId.
Method(s)
1pubnub.dataSync.removeMembership(
2 String membershipId, {
3 String? ifMatchesEtag,
4 Keyset? keyset,
5 String? using,
6}) // Future<RemoveMembershipResult>
| Parameter | Description |
|---|---|
membershipId *Type: string Default: n/a | Membership 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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200
3}
Entities
Entities are instances of the custom entity classes you define on your keyset. In the running example, product is a class and product-sneaker-42 is an instance. Use Create a new entity class to declare a class, and Get entity class by ID 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('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)
1pubnub.dataSync.createEntity(
2 EntityInput entity, {
3 Keyset? keyset,
4 String? using,
5}) // Future<CreateEntityResult>
| Parameter | Description |
|---|---|
entity *Type: EntityInput Default: n/a | The entity to create. |
> idType: string Default: server-generated | Entity identifier. Omit to let the server generate a UUID. Max 255 characters. |
> className *Type: string Default: n/a | Name of the entity class this instance belongs to. Set at creation and immutable afterward. Sent to the service as entityClass. |
> classVersion *Type: int Default: n/a | Version of the entity class schema. Sent to the service as entityClassVersion. |
> classLevelType: ClassLevel Default: service default | Class hierarchy level of className, either ClassLevel.global for a class the service provides or ClassLevel.subKey for one defined on your keyset. Used to disambiguate classes with the same name defined at different levels. Set at creation and immutable afterward. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>Default: n/a | Free-form JSON object holding your application data. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 201,
3 "data": {
4 "id": "product-sneaker-42",
5 "entityClass": "product",
6 "entityClassVersion": 1,
7 "entityClassLevel": "SubKey",
8 "payload": { "name": "Retro Sneaker", "price": 89.99, "stock": 12 },
9 "createdAt": "2026-07-13T09:15:00.000Z",
10 "updatedAt": "2026-07-13T09:15:00.000Z",
11 "eTag": "StUvWxYzAbCdEf"
12 }
13}
Get entity
Returns a single entity by entityId.
Method(s)
1pubnub.dataSync.getEntity(
2 String entityId, {
3 Keyset? keyset,
4 String? using,
5}) // Future<GetEntityResult>
| Parameter | Description |
|---|---|
entityId *Type: string Default: n/a | Entity identifier. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "product-sneaker-42",
5 "entityClass": "product",
6 "entityClassVersion": 1,
7 "entityClassLevel": "SubKey",
8 "payload": { "name": "Retro Sneaker", "price": 89.99, "stock": 12 },
9 "createdAt": "2026-07-13T09:15:00.000Z",
10 "updatedAt": "2026-07-13T09:15:00.000Z",
11 "eTag": "StUvWxYzAbCdEf"
12 }
13}
Get all entities
Returns a paginated list of entities within a class. The className parameter is required, entities are always listed within the context of their class. For pagination, filtering, and sorting, refer to sorting and pagination.
Method(s)
1pubnub.dataSync.getEntities(
2 String className, {
3 int? classVersion,
4 ClassLevel? classLevel,
5 String? cursor,
6 int? limit,
7 String? filterFast,
8 String? filter,
9 String? sort,
10 Keyset? keyset,
11 String? using,
12}) // Future<GetEntitiesResult>
| Parameter | Description |
|---|---|
className *Type: string Default: n/a | Name of the entity class to list. |
classVersionType: int Default: latest version | Entity class version to list. Omit to use the latest version of the class. |
classLevelType: ClassLevel Default: service default | Class hierarchy level of className, either ClassLevel.global for a class the service provides or ClassLevel.subKey for one defined on your keyset. Used to disambiguate a class name defined at both levels. |
cursorType: string Default: n/a | Opaque pagination cursor. Omit for the first page. |
limitType: int Default: 20 | Maximum number of entities per page. Max 100. |
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. |
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. |
sortType: string Default: n/a | Order results. Pass a comma-separated list of fields each optionally suffixed with :asc or :desc. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": [
4 {
5 "id": "product-sneaker-42",
6 "entityClass": "product",
7 "entityClassVersion": 1,
8 "entityClassLevel": "SubKey",
9 "payload": { "name": "Retro Sneaker", "price": 89.99, "stock": 12 },
10 "createdAt": "2026-07-13T09:15:00.000Z",
11 "updatedAt": "2026-07-13T09:15:00.000Z",
12 "eTag": "StUvWxYzAbCdEf"
13 }
14 ],
15 "meta": {
show all 20 linesOther examples
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). Reference a declared property by its name, not its path, and combine conditions with && and ||. It shares the same expression language as filter, so only one of the two can be sent per call.
1
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 ||. It shares the same expression language as filterFast, 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 nextCursor from the result and pass it back as cursor on the next call. Stop when hasNext is false.
1
Set entity
Replaces an entity in full (PUT). Send the complete set of fields, any field you omit is cleared. The class is immutable after creation, so EntityUpdate doesn't expose it and there's nothing to send. Refer to optimistic concurrency with ETags for ifMatchesEtag. For a partial update, use Update entity.
Method(s)
1pubnub.dataSync.setEntity(
2 String entityId,
3 EntityUpdate entity, {
4 String? ifMatchesEtag,
5 Keyset? keyset,
6 String? using,
7 }
8) // Future<SetEntityResult>
| Parameter | Description |
|---|---|
entityId *Type: string Default: n/a | Entity identifier. |
entity *Type: EntityUpdate Default: n/a | The replacement entity data. The entity class is immutable and cannot be included. |
> classVersion *Type: int Default: n/a | Version of the entity class schema. Sent to the service as entityClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "product-sneaker-42",
5 "entityClass": "product",
6 "entityClassVersion": 1,
7 "entityClassLevel": "SubKey",
8 "payload": { "name": "Retro Sneaker", "price": 79.99, "stock": 12 },
9 "createdAt": "2026-07-13T09:15:00.000Z",
10 "updatedAt": "2026-07-13T13:00:00.000Z",
11 "eTag": "UvWxYzAbCdEfGh"
12 }
13}
Update entity
Applies a partial update to an entity. Paths can target fields inside payload or top-level stored fields. Refer to partial update for the JSON Pointer rules.
Method(s)
1pubnub.dataSync.updateEntity(
2 String entityId, {
3 Map<String, dynamic>? add,
4 Map<String, dynamic>? replace,
5 List<String>? remove,
6 List<JsonPointerPair>? move,
7 List<JsonPointerPair>? copy,
8 Map<String, dynamic>? test,
9 String? ifMatchesEtag,
10 Keyset? keyset,
11 String? using,
12}) // Future<UpdateEntityResult>
| Parameter | Description |
|---|---|
entityId *Type: string Default: n/a | Entity identifier. |
addType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
replaceType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
removeType: List <String>Default: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
moveType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is removed and re-added at path. Provide at least one patch operation. |
copyType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is duplicated to path. Provide at least one patch operation. |
testType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. Provide at least one patch operation. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The patch succeeds only if it still matches, otherwise the server returns 412. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
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.
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "product-sneaker-42",
5 "entityClass": "product",
6 "entityClassVersion": 1,
7 "entityClassLevel": "SubKey",
8 "payload": { "name": "Retro Sneaker", "price": 79.99, "stock": 8 },
9 "createdAt": "2026-07-13T09:15:00.000Z",
10 "updatedAt": "2026-07-13T13:05:00.000Z",
11 "eTag": "WxYzAbCdEfGhIj"
12 }
13}
Other examples
Combine patch operations, and guard the write with ifMatchesEtag
A single updateEntity call can mix add, replace, remove, move, copy, and test in one call. All operations in the call apply together or not at all. 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.
1
Remove entity
Deletes an entity by entityId.
Method(s)
1pubnub.dataSync.removeEntity(
2 String entityId, {
3 String? ifMatchesEtag,
4 Keyset? keyset,
5 String? using,
6}) // Future<RemoveEntityResult>
| Parameter | Description |
|---|---|
entityId *Type: string Default: n/a | Entity 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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200
3}
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 you define on your keyset. 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 to declare a class, and Get relationship class by name and version 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)
1pubnub.dataSync.createRelationship(
2 RelationshipInput relationship, {
3 Keyset? keyset,
4 String? using,
5}) // Future<CreateRelationshipResult>
| Parameter | Description |
|---|---|
relationship *Type: RelationshipInput Default: n/a | The relationship to create. |
> idType: string Default: server-generated | Relationship identifier. Omit to let the server generate a UUID. Max 255 characters. |
> entityAId *Type: string Default: n/a | Identifier of the first linked entity. Immutable after creation. |
> entityBId *Type: string Default: n/a | Identifier of the second linked entity. Immutable after creation. |
> className *Type: string Default: n/a | Name of the relationship class this instance belongs to. Set at creation and immutable afterward. Sent to the service as relationshipClass. |
> classVersion *Type: int Default: n/a | Version of the relationship class schema. Sent to the service as relationshipClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>Default: n/a | Free-form JSON object holding your application data. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 201,
3 "data": {
4 "id": "rel-bob-owns-sneaker-42",
5 "entityAId": "seller-bob",
6 "entityBId": "product-sneaker-42",
7 "relationshipClass": "ProductOwner",
8 "relationshipClassVersion": 1,
9 "payload": { "since": "2026-07-13" },
10 "createdAt": "2026-07-13T09:20:00.000Z",
11 "updatedAt": "2026-07-13T09:20:00.000Z",
12 "eTag": "YzAbCdEfGhIjKl"
13 }
14}
Get relationship
Returns a single relationship by relationshipId.
Method(s)
1pubnub.dataSync.getRelationship(
2 String relationshipId, {
3 Keyset? keyset,
4 String? using,
5}) // Future<GetRelationshipResult>
| Parameter | Description |
|---|---|
relationshipId *Type: string Default: n/a | Relationship identifier. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "rel-bob-owns-sneaker-42",
5 "entityAId": "seller-bob",
6 "entityBId": "product-sneaker-42",
7 "relationshipClass": "ProductOwner",
8 "relationshipClassVersion": 1,
9 "payload": { "since": "2026-07-13" },
10 "createdAt": "2026-07-13T09:20:00.000Z",
11 "updatedAt": "2026-07-13T09:20:00.000Z",
12 "eTag": "YzAbCdEfGhIjKl"
13 }
14}
Get all relationships
Returns a paginated list of relationships within a class. The className 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.
Method(s)
1pubnub.dataSync.getRelationships(
2 String className, {
3 int? classVersion,
4 String? entityAId,
5 String? entityBId,
6 String? cursor,
7 int? limit,
8 String? filterFast,
9 String? filter,
10 String? sort,
11 Keyset? keyset,
12 String? using,
13}) // Future<GetRelationshipsResult>
| Parameter | Description |
|---|---|
className *Type: string Default: n/a | Name of the relationship class to list. |
classVersionType: int Default: latest version | Relationship class version to list. Omit to use the latest version of the class. |
entityAIdType: string Default: n/a | List only relationships whose first entity is this id. |
entityBIdType: string Default: n/a | List only relationships whose second entity is this id. |
cursorType: string Default: n/a | Opaque pagination cursor. Omit for the first page. |
limitType: int Default: 20 | Maximum number of relationships per page. Max 100. |
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. |
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. |
sortType: string Default: n/a | Order results. Pass a comma-separated list of fields each optionally suffixed with :asc or :desc. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": [
4 {
5 "id": "rel-bob-owns-sneaker-42",
6 "entityAId": "seller-bob",
7 "entityBId": "product-sneaker-42",
8 "relationshipClass": "ProductOwner",
9 "relationshipClassVersion": 1,
10 "payload": { "since": "2026-07-13" },
11 "createdAt": "2026-07-13T09:20:00.000Z",
12 "updatedAt": "2026-07-13T09:20:00.000Z",
13 "eTag": "YzAbCdEfGhIjKl"
14 }
15 ],
show all 21 linesOther 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.
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). Reference a declared property by its name, not its path. It shares the same expression language as filter, so only one of the two can be sent per call.
1
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. It shares the same expression language as filterFast, 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 nextCursor from the result and pass it back as cursor on the next call. Stop when hasNext is false.
1
Set relationship
Replaces a relationship in full (PUT). Send the complete set of fields, any field you omit is cleared. The linked entities are immutable, so RelationshipUpdate doesn't expose entityAId or entityBId at all, there's nothing to resend. Refer to optimistic concurrency with ETags for ifMatchesEtag. For a partial update, use Update relationship.
Method(s)
1pubnub.dataSync.setRelationship(
2 String relationshipId,
3 RelationshipUpdate relationship, {
4 String? ifMatchesEtag,
5 Keyset? keyset,
6 String? using,
7 }
8) // Future<SetRelationshipResult>
| Parameter | Description |
|---|---|
relationshipId *Type: string Default: n/a | Relationship identifier. |
relationship *Type: RelationshipUpdate Default: n/a | The replacement relationship data. The linked entities and the relationship class are immutable and cannot be included. |
> classVersion *Type: int Default: n/a | Version of the relationship class schema. Sent to the service as relationshipClassVersion. |
> statusType: string Default: n/a | Free-form lifecycle status. Max 100 characters. |
> payloadType: Map <String, dynamic>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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "rel-bob-owns-sneaker-42",
5 "entityAId": "seller-bob",
6 "entityBId": "product-sneaker-42",
7 "relationshipClass": "ProductOwner",
8 "relationshipClassVersion": 1,
9 "payload": { "since": "2026-07-13", "tier": "gold" },
10 "createdAt": "2026-07-13T09:20:00.000Z",
11 "updatedAt": "2026-07-13T14:00:00.000Z",
12 "eTag": "AbCdEfGhIjKlMn"
13 }
14}
Update relationship
Applies a partial update to a relationship. Paths can target fields inside payload or top-level stored fields. Refer to partial update for the JSON Pointer rules.
Method(s)
1pubnub.dataSync.updateRelationship(
2 String relationshipId, {
3 Map<String, dynamic>? add,
4 Map<String, dynamic>? replace,
5 List<String>? remove,
6 List<JsonPointerPair>? move,
7 List<JsonPointerPair>? copy,
8 Map<String, dynamic>? test,
9 String? ifMatchesEtag,
10 Keyset? keyset,
11 String? using,
12}) // Future<UpdateRelationshipResult>
| Parameter | Description |
|---|---|
relationshipId *Type: string Default: n/a | Relationship identifier. |
addType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to values to add. Provide at least one patch operation. |
replaceType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to replacement values. Provide at least one patch operation. |
removeType: List <String>Default: n/a | Full JSON Pointers to remove. Provide at least one patch operation. |
moveType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is removed and re-added at path. Provide at least one patch operation. |
copyType: List <JsonPointerPair>Default: n/a | Pairs of from and path JSON Pointers. The value at from is duplicated to path. Provide at least one patch operation. |
testType: Map <String, dynamic>Default: n/a | Full JSON Pointers mapped to expected values. The patch fails if any value does not match. Provide at least one patch operation. |
ifMatchesEtagType: string Default: n/a | The eTag from a prior read. The patch succeeds only if it still matches, otherwise the server returns 412. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
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.
Sample code
Reference code
1
Response
1{
2 "status": 200,
3 "data": {
4 "id": "rel-bob-owns-sneaker-42",
5 "entityAId": "seller-bob",
6 "entityBId": "product-sneaker-42",
7 "relationshipClass": "ProductOwner",
8 "relationshipClassVersion": 1,
9 "payload": { "since": "2026-07-13", "tier": "platinum" },
10 "createdAt": "2026-07-13T09:20:00.000Z",
11 "updatedAt": "2026-07-13T14:05:00.000Z",
12 "eTag": "CdEfGhIjKlMnOp"
13 }
14}
Remove relationship
Deletes a relationship by relationshipId.
Method(s)
1pubnub.dataSync.removeRelationship(
2 String relationshipId, {
3 String? ifMatchesEtag,
4 Keyset? keyset,
5 String? using,
6}) // Future<RemoveRelationshipResult>
| Parameter | Description |
|---|---|
relationshipId *Type: string Default: n/a | Relationship 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. |
keysetType: Keyset Default: default keyset | Override for the PubNub default keyset configuration. |
usingType: string Default: n/a | Keyset name from the keysetStore to be used for this method call. |
Sample code
Reference code
1
Response
1{
2 "status": 200
3}
Real-time updates
DataSync objects can publish create, update, and delete events that you receive in real time on the dataSync stream of a Subscription. Create a subscription to the object with pubnub.dataSyncUser(id), pubnub.dataSyncChannel(id), or pubnub.dataSyncEntity(id), call subscribe() on it, and listen to subscription.dataSync, as described in Add DataSync listener. A membership or relationship has no handle of its own, because it never publishes on a channel named after its own id. Subscribe through the user, channel, or entity it connects.
1
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, which the typed getters id, status, payload, eTag, createdAt, updatedAt, and expiresAt read from. For a delete event, only id and deletedAt are set.
objectType names the object kind directly, while the state always arrives in the same data map. Dispatch on objectType rather than on which getters return a value, because one event type serves all five kinds. className tells you the specific class within a kind, for example which subclass of User an event belongs to. Membership events carry channelId and userId, and relationship events carry entityAId and entityBId.
Where each event is delivered
The channels that receive an event depend on the changed object. A relationship or membership never publishes on a channel named after its own id:
| Change to | On create | On update or delete |
|---|---|---|
| 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 | Same as create |
| A relationship | Both the entityAId and the entityBId of the relationship | Same as create |
Update and delete also fan out to connected objects
An update or delete on a user, channel, or entity is also delivered on the id channel of every other object connected to it by a relationship or membership, in either direction, at the time of the change. A client subscribed to a connected object's channel receives the event too, even though that object itself did not change. A create does not fan out this way, because nothing can be connected to a brand new object yet.
The connected channels are entity, user, and channel ids. Relationships and memberships never get an id channel of their own, so they never appear in this fan-out as a channel name.
So to see memberships appear and disappear for Alice, subscribe to user-alice rather than the membership id. A client subscribed to both sides of the same link receives the change twice, once per channel. Deduplicate create and update events on the object's id and updatedAt, and delete events on id and deletedAt.
Projection channels
Each projection declared on the class gets its own event, published to its own channel, and carrying only that projection's view of the payload:
| Projection | Channel | Payload |
|---|---|---|
__default__ (the base projection) | The object's own id, for example product-sneaker-42 | The fields tagged __default__ |
Any named projection, for example admin | __<projection>__<id>, for example __admin__product-sneaker-42 | The fields tagged with that projection |
Filtering applies to payload and to status: each is included in a given projection's event only if the class declares it under that projection, falling back to __default__ when the class never declares status. id, eTag, createdAt, updatedAt, and expiresAt are never filtered. Every projection's event carries them unchanged.
So one change to an object whose class declares an admin projection publishes two events, and a client subscribed only to product-sneaker-42 never sees the admin-only fields.
To observe a projection, pass its name as projection to subscription() on a DataSync handle. The SDK subscribes to __<projection>__<id> for you. You can also pass withProjection to pubnub.subscribe() or pubnub.subscription(), which applies to channels only and not to channelGroups. The rules are:
- A
nullor blank projection,default, and__default__all refer to the base projection, so the object's own id is subscribed as it is. - Ids are used verbatim, so wildcards such as
customer.*work. Don't pass an id that already carries a projection prefix. - To observe several projections of the same object, create one subscription per projection.
Subscription.projectionreturns the projection a subscription observes, ornullfor the base projection.
1
If the class declares no properties at all there is nothing to filter, so a single unfiltered event is published to the object's own id channel.
Projection channels need channel grants
Publishing to __<projection>__<id> is an ordinary channel publish, and it isn't gated by the projection assignments in a token. Use Access Manager grants on ResourceType.channel to control who can subscribe to a projection channel. Anyone who can subscribe there receives the restricted fields. Refer to Grant token.
Delete events are never filtered by a projection.
Events are off by default and are enabled per class in the Admin Portal. Refer to enabling events for how to turn them on, and receiving events for the listener flow.