On this page

DataSync data model

DataSync stores everything with two primitives:

  • entities (objects)
  • relationships (links between two objects).

Both are typed by classes. Users, channels, and memberships are built-in classes, so everything on this page applies to them too.

Entities

A DataSync entity is a JSON object made up of system fields and a payload field that holds your free-form application data. Every entity is an instance of exactly one entity class, and records which version of that class it currently pins to. The class name is fixed at creation, while the version can be changed later.

Don't confuse this with an SDK entity

An entity here is a stored server-side record. An SDK entity is a local handle you create on the client, with no stored state of its own.

The entity endpoints are Get entities, Get entity, Create entity, Update entity, Patch entity, and Delete entity.

System fields on every entity:

FieldPurpose
id
The entity's identifier
status
A free-form application lifecycle string, 1 to 100 characters
eTag
Optimistic concurrency marker, see Data operations
createdAt
Creation timestamp
updatedAt
Last modification timestamp
expiresAt
Expiry timestamp, see Data expiry (TTL)
entityClass
The name of the entity class this instance is an instance of
entityClassVersion
The class version this entity instance pins to
entityClassLevel
Whether that class is Global or SubKey (keyset-level), see Global classes and SubKey classes

The payload field sits alongside these system fields, at the same level, not nested inside them. It is not itself a system field. It is where your application data lives. See Payloads below for what it can contain.

You can supply your own id on create, or let the server generate a UUID for you. An id must be 1 to 255 characters and can't contain whitespace, commas, colons, asterisks, forward slashes, backslashes, or control characters. It also has to be unique within the keyset: supplying an id that is already taken is rejected with a 409 Conflict.

An id becomes the name of the channel the object's events publish to, so the ids you pick decide what a client can subscribe to. Periods are allowed, which means dot-namespaced ids such as product.sneaker-42 can be followed as a group with Wildcard Subscribe.

In Bob's marketplace, product-sneaker-42 is an entity, an instance of the Product class, with a payload of { "name": "Retro Sneaker", "price": 89.99, "stock": 12 }. The full entity looks like this, with the system fields and the payload field as siblings:

{
"id": "product-sneaker-42",
"status": "active",
"eTag": "a1b2c3",
"createdAt": "2026-06-01T10:00:00Z",
"updatedAt": "2026-07-03T09:15:00Z",
"expiresAt": "2026-08-01T00:00:00Z",
"entityClass": "Product",
"entityClassVersion": 1,
"entityClassLevel": "SubKey",
"payload": { "name": "Retro Sneaker", "price": 89.99, "stock": 12 }
}

The system fields and payload are peers, top-level fields on the same object. Your application data nests under payload, not inside the system fields:

Relationships

A relationship is a typed link between two entities, entity A and entity B, with its own payload field. Every relationship is an instance of exactly one relationship class.

The relationship endpoints are Get relationships, Get relationship, Create relationship, Update relationship, Patch relationship, and Delete relationship.

System fields on every relationship:

FieldPurpose
id
The relationship's identifier
entityAId
The linked entity A's id, immutable after creation
entityBId
The linked entity B's id, immutable after creation
status
A free-form application lifecycle string, 1 to 100 characters
eTag
Optimistic concurrency marker, see Data operations
createdAt
Creation timestamp
updatedAt
Last modification timestamp
expiresAt
Expiry timestamp, see Data expiry (TTL)
relationshipClass
The name of the relationship class this instance is an instance of
relationshipClassVersion
The class version this relationship instance pins to

The two linked entity ids are inputs you supply when you create the relationship, alongside its class name and class version. They can't be changed afterwards.

Memberships name their sides

A membership is a relationship, but it exposes its two sides as channelId and userId rather than the generic entityAId and entityBId. The channel is entity A and the user is entity B. Refer to Users, channels, and memberships.

Relationship classes declare a cardinality (one-to-one, one-to-many, or many-to-many), and DataSync enforces it when a relationship is created:

  • One-to-one: an entity can participate in at most one relationship instance of the class, on either side. If seller-bob and product-sneaker-42 are linked, neither can appear in another relationship of that class.
  • One-to-many: an entity is bound to whichever side, A or B, it first appears on for that class. Once bound, it can appear on that same side any number of times, but never on the other side. In Bob's marketplace, a ProductOwner class is one-to-many: the first time seller-bob is used as entity A, he's locked to side A for ProductOwner, and every product he owns is linked as entity B. A relationship that would put a product on side A, or put Bob on side B, is rejected. Neither side is capped, though, so a product on side B can be linked to more than one seller. Beyond the side binding, the only other rule is that the same ordered pair can't be created twice. If you need an at-most-one-owner rule, use one-to-one or enforce it in your application.
  • Many-to-many: no side constraints. The same entity can appear on either side any number of times. Only the exact ordered pair, the same entity A together with the same entity B, must be unique within the class. The reversed pair, that same entity as B linked to the other as A, is a separate relationship and is allowed.

A relationship that violates its class's cardinality is rejected with a 409 Conflict. Both linked entities must already exist, or the request is rejected with a 404 Not Found.

The one-to-many side-binding rule is easiest to see in Bob's marketplace. Once seller-bob is used as entity A for ProductOwner, he's locked to side A: he can own any number of products (product-sneaker-42, product-cap-7, and more, each linked as entity B), but a relationship that puts a product on side A, or puts Bob on side B, is rejected.

Side B is bound the same way, and it isn't capped either. A second seller, bound to side A in their own first ProductOwner relationship, can be linked to product-sneaker-42 as well. Cardinality gives you the side rule, not an exclusive-owner rule:

A relationship expires when the earlier of its two entities' expiry times arrives, computed once when the relationship is created. See Data expiry (TTL).

Classes

A class is a versioned type: a name plus an integer version. Multiple versions of the same class can coexist. Each instance records which version it currently pins to: you choose the version at creation, and you can move an instance to another existing version of the same class by setting entityClassVersion or relationshipClassVersion on an update or a partial update. The class name itself can't change.

A class carries property definitions that declare which payload fields are indexed, how they can be filtered and sorted, and which projections they belong to. The Schemas and validation page covers this in depth.

Every class is described by two independent things: what it types (an entity or a relationship) and who owns it (PubNub or you). These are separate axes, not alternatives, so every class has one answer on each. A class is always an entity class or a relationship class, and its entityClassLevel (or relationshipClassLevel) is always Global or SubKey (keyset-level).

Entity classes and relationship classes

This axis is about what the class types:

Global classes and SubKey classes

This axis is about who defines and owns the class:

  • Global classes (entityClassLevel: Global) are seeded by PubNub and shared. The built-in User, Channel, and Membership classes are Global, and you can't modify them.
  • SubKey classes (entityClassLevel: SubKey), also called keyset-level classes, are the ones you define on your own keyset, like Product and ProductOwner in Bob's marketplace.

The two axes together

The two axes combine, so every class falls into one of four cells:

Global (PubNub-owned)SubKey (keyset-level, yours)
Entity class
User, Channel
Product
Relationship class
Membership
ProductOwner

Classes are defined and managed with the Admin API. Entity classes have their own endpoints, list, read, create, replace, and delete, and so do relationship classes, list, read, create, replace, and delete. Partial entity-class updates aren't implemented; replace the complete class version instead.

Built-in classes: users, channels, and memberships

User and Channel are Global entity classes, and Membership is a Global relationship class linking a channel to a user, many-to-many. When you create a user, a channel, or a membership, the API sets the class for you.

A User entity's id and a Channel entity's id share the same identifier as the UUID and channel name used elsewhere on PubNub, so you can attach structured data to a UUID or channel your app already publishes, subscribes, or shows Presence on. They're not the same stored record, though: DataSync keeps its own entity, separate from Presence and Publish/Subscribe state, so a UUID or channel works for messaging and Presence whether or not a matching entity exists in DataSync, and creating, updating, or deleting the entity has no effect on that UUID's or channel's ability to publish, subscribe, or appear in Presence.

Refer to Users, channels, and memberships for the operational details.

How it fits together

Back to Bob's marketplace, the Product entity class defines the shape of product-sneaker-42, the Membership relationship class defines the shape of Alice's membership in channel-summer-sale, and that membership links the channel-summer-sale entity to the user-alice entity.

Payloads

The payload is the value of the payload field on an entity or relationship. It's a free-form, arbitrarily nested JSON object holding your application data, and it's stored as you send it. It has to be a JSON object: an array or a scalar is rejected.

What is enforced comes from the class's property declarations. A value at a declared property path must be a scalar of the declared type, and a non-nullable property must be present. Violations return a 400. Fields you haven't declared are stored as-is and aren't checked.

Individual payload fields are addressable by JSON Pointer, which is what makes partial updates possible (see Data operations) and how you declare fields at the class level (see Schemas and validation).

Both cases use the same coordinate space. A pointer starts at the whole object, where payload sits alongside the system fields, so the price on a product is /payload/price whether you're writing a partial update or declaring a class property.

A class property can point at a nested field, not just a top-level one, for example /payload/dimensions/weight.

Declare the fields you filter on

Only payload fields declared as class properties with a filtering mode other than none are searchable, and you reference them by the property's name, not by its JSON Pointer path. Eventually consistent search (filter) additionally requires filtering mode full. If you plan to filter or sort on a field, declare it as a property on the class first. See Schemas and validation.

The four built-in fields id, createdAt, updatedAt, and status are the exception. They're always filterable and sortable, with no declaration needed. See Built-in fields.

Creating product-sneaker-42 as an instance of the product class, version 1:

1const response = await pubnub.dataSync.createEntity({
2 id: 'product-sneaker-42',
3 class: 'product',
4 data: {
5 classVersion: 1,
6 payload: { name: 'Retro Sneaker', price: 89.99, stock: 12 },
7 },
8})

Refer to Create entity (JavaScript), Create entity (C#), and Create entity (REST) for the full parameter reference, and to Creating objects for what each object kind requires.