DataSync events
DataSync events are regular PubNub messages delivered on regular channels. Your app receives them the same way it already does for messaging.
In Bob's marketplace, Bob drops the price on product-sneaker-42, and every shopper watching the sale (including Alice) sees the new price without refreshing or polling. Any create, update, or delete of a DataSync object can publish an event like this.
Enable DataSync events
Events are off by default. You turn them on per class version, per keyset, in the Admin Portal, using event rules. A rule is all-or-nothing: it enables create, update, and delete events together, and there's no way to select individual verbs.
Events are off by default
To receive DataSync events, enable event publishing for the class version in the Admin Portal. With no event rule configured for a class version, no events fire.
Because rules are per class version, publishing a new version of a class stops its events until you add a rule for the new version too.
This applies to entities and relationships you define, and to the built-in users, channels, and memberships. Each is governed by the event rule on its own class version. To see which classes and versions exist on your keyset, list them with Get all entity class entries and Get all relationship classes.
Enable events in the Admin Portal
The event toggle lives on the keyset's DataSync configuration:
- In the Admin Portal, go to Apps & Keysets and select your app.
- Select the keyset you want to configure.
- In the keyset details, scroll to the Configuration section and open the DataSync tab.
- Turn on Enable DataSync to stream entity state changes as real-time events.
- Under Select entities to sync, choose which classes publish events. Only the classes you select emit events, which is how events stay off by default until you opt a class in. Relationship classes, including the built-in
Membership, need rules of their own, so a keyset that enables only entity classes receives no membership events. Each class version is enabled separately.
Enabling a rule isn't retroactive
A rule is read when the write happens, and the result is cached for up to five minutes. Turning a rule on can take that long to affect new writes, and it never applies to writes that already happened. Objects written while events were off don't produce events later.
Region is set once
The DataSync tab is also where you pick the keyset's data Region. Once set, the region can't be changed. Refer to Configuration.
Where events are published
Every entity has an ID channel, named after its own ID, and every event about that entity publishes there.
Entity create, update, and delete events
For an entity:
- A create event publishes only to the entity's ID channel. A newly created entity has no relationships yet, so there's nowhere else to reach.
- An update or delete event publishes to the entity's ID channel, plus the ID channel of every other entity it's currently linked to by a relationship.
Relationship events route differently, see Membership events below.
In Bob's marketplace, once seller-bob owns product-sneaker-42 through a ProductOwner relationship, an update to product-sneaker-42 publishes to both the product-sneaker-42 channel and the seller-bob channel.
Membership events
Relationships, including memberships, have no ID channel of their own. Every relationship event, create, update, or delete, publishes to both of its linked entities' ID channels instead.
In Bob's marketplace, an update to product-sneaker-42 arrives on the product-sneaker-42 channel, and on the ID channel of every entity product-sneaker-42 is linked to through a relationship. A membership event arrives on both the linked user's channel and the linked channel's ID channel, since memberships have no channel of their own.
A delete event follows the same routing as any other event. For an entity, it's published to its own ID channel plus the ID channels of its linked entities. For a relationship or membership, it's published to the ID channels of both linked entities.
Deleting an entity also deletes the relationships linked to it, and each of those cascaded deletes publishes an event of its own. Deleting an entity that has three relationships produces one entity-delete event plus three relationship-delete events, so expect a burst rather than a single message.
Projection channels
An event's payload is scoped to the channel it arrives on
The object's own ID channel carries the __default__ view of the payload. Each named projection declared on the class has a channel of its own, __<projection>__<id>, carrying that projection's view. The projections a class declares are listed on its entity class or relationship class definition, per property.
Every change fans out to one event per projection declared on the class, and each copy is filtered to that projection. In Bob's marketplace, where phone belongs only to the admin projection, a subscriber to user-alice never receives phone. A client that needs phone in real time subscribes to __admin__user-alice instead.
status is scoped the same way payload is. A class that declares no /status property treats it as __default__ only, so it drops out of named-projection channels. Declare /status tagged with a projection (see Reading objects) to include it there too. The other system fields, id, eTag, createdAt, updatedAt, and expiresAt, are identical on every channel. A delete event has no payload or status, so the same { id, deletedAt } body reaches every one of those channels. If a class declares no properties at all, a single unfiltered event is published to the object's own ID channel.
Publishing to a projection channel is an ordinary channel publish, and a token's projection entries don't gate it. Use Access Manager channel grants to control who can subscribe to a projection channel. See Projections and events.
Subscribe to what you display
To stay current on one entity, subscribe to its ID channel. To follow a set of entities, subscribe to each of their ID channels.
A relationship or membership has no ID channel of its own, so to follow one, subscribe to the channels of the two entities it links.
If the class declares named projections and you need one of those views, subscribe to __<projection>__<id> as well. In an SDK that ships DataSync SDK entities, pass the projection name as a subscription option instead of writing the prefixed channel name yourself.
Event format
A DataSync event is a PubNub message with message type 5. This distinguishes it from an ordinary message. For more information on message types, see Message types. The envelope shape is:
{
"version": "1.0",
"metadata": {
"event": "create" | "update" | "delete",
"source": "data-sync",
"type": "user" | "channel" | "membership" | "entity" | "relationship",
"className": "Product",
"classLevel": "Global" | "SubKey",
"classVersion": 1
},
"data": { ... }
}
For create and update, data carries the object's current state, its system fields plus its payload as scoped for that channel's projection. The object's class and class version aren't repeated in data, they're in metadata.
For delete, data is exactly { id, deletedAt }, for every object kind alike. Relationship create and update events additionally carry entityAId and entityBId, and membership create and update events carry channelId and userId in their place. Delete events carry neither pair, so track the sides from the earlier create event if you need them.
note
metadata.type names the object kind directlyEach of the five object kinds has its own type value: user, channel, membership, entity, and relationship. A user reports user rather than entity, and a membership reports membership rather than relationship, so you can dispatch on type alone without matching class names.
The value is derived from the Global class your class descends from, not from its name, so a class of your own that extends the built-in User reports type: "user" alongside its own className and classLevel: "SubKey", and a class of your own merely named User that extends nothing reports type: "entity".
Fields that have no value are left out of data rather than sent as null, so status and payload are absent on an object that has neither. Read every field except id defensively.
Events from Bob's marketplace look like this:
- Update event
- Delete event
- Membership create event
{
"version": "1.0",
"metadata": {
"event": "update",
"source": "data-sync",
"type": "entity",
"className": "Product",
"classLevel": "SubKey",
"classVersion": 1
},
"data": {
"id": "product-sneaker-42",
"status": "active",
"eTag": "a1b2c3",
"createdAt": "2026-06-01T10:00:00Z",
show all 20 lines{
"version": "1.0",
"metadata": {
"event": "delete",
"source": "data-sync",
"type": "entity",
"className": "Product",
"classLevel": "SubKey",
"classVersion": 1
},
"data": {
"id": "product-sneaker-42",
"deletedAt": "2026-07-03T09:20:00Z"
}
}
A membership event carries type: "membership" and names its sides channelId and userId, matching the field names the membership REST resource uses:
{
"version": "1.0",
"metadata": {
"event": "create",
"source": "data-sync",
"type": "membership",
"className": "Membership",
"classLevel": "Global",
"classVersion": 1
},
"data": {
"id": "membership-alice-sale",
"status": "active",
"eTag": "d4e5f6",
"createdAt": "2026-07-03T09:15:00Z",
show all 22 linesA membership event never carries entityAId or entityBId. A relationship of your own carries those two instead and never channelId or userId.
| Field | Description |
|---|---|
version | Envelope version, 1.0 for DataSync events |
metadata.event | create, update, or delete |
metadata.source | Always data-sync |
metadata.type | The object kind: user, channel, membership, entity, or relationship |
metadata.className | The class name, for example Product or User. Read it together with classLevel to tell a built-in class from one of your own that happens to share its name |
metadata.classLevel | The class's level, Global or SubKey |
metadata.classVersion | The class version the object pins to |
data | On create and update: id, createdAt, updatedAt, expiresAt, eTag, status, and payload, plus entityAId and entityBId for relationships, or channelId and userId for memberships. On delete: exactly { id, deletedAt }, for every object kind alike |
className is the plain class name. The class's level travels in a separate field, classLevel. A class you define on your keyset reports classLevel: "SubKey", and a built-in class like User reports classLevel: "Global".
Reading the same update event shown above:
- JavaScript
- C#
- curl
1subscription.onDataSync = (event) => {
2 const change = event.message
3 console.log(change.event, change.className, change.data.payload?.price)
4}
The JS SDK flattens the wire envelope: metadata.event arrives as event.message.event, metadata.className as event.message.className, and so on. event.message.data is the raw data object shown above.
1var listener = new SubscribeCallbackExt(
2 (Pubnub pn, PNDataSyncEventResult dataSyncEvent) =>
3 {
4 Console.WriteLine(dataSyncEvent.Event);
5 Console.WriteLine(dataSyncEvent.ClassName);
6 Console.WriteLine(dataSyncEvent.EntityData?.Payload["price"]);
7 },
8 (Pubnub pn, PNStatus status) => { /* handle status */ });
The C# SDK exposes the same fields as properties on PNDataSyncEventResult: Event, ClassName, ClassLevel, ClassVersion, and Source, with the object state on EntityData (or RelationshipData for memberships and relationships).
1curl -N 'https://ps.pndsn.com/v2/subscribe/{subKey}/product-sneaker-42/0?tt=0&auth=<token>'
1[
2 [
3 {
4 "version": "1.0",
5 "metadata": {
6 "event": "update",
7 "source": "data-sync",
8 "type": "entity",
9 "className": "Product",
10 "classLevel": "SubKey",
11 "classVersion": 1
12 },
13 "data": {
14 "id": "product-sneaker-42",
15 "status": "active",
show all 25 linesRefer to Add DataSync listener (JavaScript) and Add DataSync listener (C#) for the full field reference of the event object each SDK exposes.
Receive DataSync events
Subscribe to the channels an object's events reach and handle messages with message type 5, the same way you handle any other message on a channel. Refer to Where events are published for which channels those are.
A common pattern is to fetch the object once to get its current state, then apply incoming events to keep that state current. An entity's channel can also receive relationship events and events about connected entities, so first check the event's object type and data.id and ignore changes for anything other than the object you're tracking. Compare eTag or updatedAt on create and update events against what you have to detect whether you missed an update and to discard duplicates. Delivery is at-least-once, so the same event can arrive more than once. Delete events carry only id and deletedAt, so use those two fields when deduplicating deletes.
Events for the same object arrive in order. Events for different objects can interleave, and there's no ordering guarantee across objects or across channels.
Subscribe with a DataSync SDK entity
SDKs that support DataSync give you a DataSync SDK entity per object kind, so you subscribe by object identifier instead of building the channel name yourself. The handle takes the identifier verbatim and resolves it to a channel:
| You create | It observes |
|---|---|
A user, channel, or entity handle for id | The id channel |
The same handle with a projection of admin | The __admin__id channel |
Passing no projection, or default or __default__, observes the object's own ID channel. Any other name observes that projection's channel. The projection is chosen per subscription rather than per handle, so one handle can serve several projections at once, and each one subscribes and unsubscribes on its own. Both subscriptions receive their own copy of a change, with the projected one carrying whatever extra fields that projection exposes.
There's no presence channel for a DataSync object, so a receivePresenceEvents option has no effect on these handles.
A relationship or membership handle receives nothing for the link itself
Relationship and membership events never reach a channel named after the relationship or membership id, so subscribe to the two linked entities instead. Refer to Membership events.
For SDK-specific method names and examples, refer to JavaScript, which provides DataSync SDK entities, or C#, where you subscribe to the object's data channel with a Channel SDK entity and attach a listener to the resulting Subscription, a SubscriptionSet, or the PubNub client. These are the only two SDKs with DataSync support, refer to SDK support.
Follow a group of objects with a wildcard
Because a handle uses the identifier verbatim, a dot-namespaced identifier ending in .* subscribes to a channel pattern and one subscription covers every matching object. Subscribing to product.* delivers changes for product.sneaker-42 and product.cap-7 alike, and __admin__product.* does the same for the admin projection. This is ordinary Wildcard Subscribe.
An event delivered through a wildcard reports the concrete delivery channel rather than the pattern. Because fan-out can deliver a change on a connected object's channel, read the changed object's id from data rather than inferring it from the channel.
- JavaScript
- C#
1const subscription = pubnub.dataSyncEntity('product.*').subscription()
2
3subscription.onDataSync = (event) => {
4 // event.channel -> 'product.sneaker-42'
5 // event.subscription -> 'product.*'
6 console.log('changed:', event.message.data.id)
7}
8
9subscription.subscribe()
1Channel channel = pubnub.Channel("product.*");
2Subscription subscription = channel.Subscription();
3
4var listener = new SubscribeCallbackExt(
5 (Pubnub pn, PNDataSyncEventResult dataSyncEvent) =>
6 {
7 string changedId = dataSyncEvent.EntityData?.Id
8 ?? dataSyncEvent.RelationshipData?.Id
9 ?? dataSyncEvent.Id;
10 Console.WriteLine("changed: " + changedId);
11 },
12 (Pubnub pn, PNStatus status) => { /* handle status */ });
13
14pubnub.AddListener(listener);
15subscription.Subscribe<object>();
Refer to DataSync SDK entities (JavaScript) and Add DataSync listener (C#) for the full method reference.