---
source_url: https://www.pubnub.com/docs/general/data-sync/data-model
title: DataSync data model
updated_at: 2026-09-08T16:52:30.000Z
---

# DataSync data model

## Documentation index

To discover more PubNub resources:

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

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

DataSync 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](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), 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.

:::note Don't confuse this with an SDK entity
An entity here is a stored server-side record. An [SDK entity](https://www.pubnub.com/docs/general/entities.md) is a local handle you create on the client, with no stored state of its own.
:::

The entity endpoints are [Get entities](https://www.pubnub.com/docs/sdks/rest-api/get-entities.md), [Get entity](https://www.pubnub.com/docs/sdks/rest-api/get-entity.md), [Create entity](https://www.pubnub.com/docs/sdks/rest-api/create-entity.md), [Update entity](https://www.pubnub.com/docs/sdks/rest-api/update-entity.md), [Patch entity](https://www.pubnub.com/docs/sdks/rest-api/patch-entity.md), and [Delete entity](https://www.pubnub.com/docs/sdks/rest-api/delete-entity.md).

System fields on every entity:

| Field | Purpose |
| --- | --- |
| `id` | The entity's identifier |
| `status` | A free-form application lifecycle string, 1 to 100 characters |
| `eTag` | Optimistic concurrency marker, see [Data operations](https://www.pubnub.com/docs/general/data-sync/data-operations.md) |
| `createdAt` | Creation timestamp |
| `updatedAt` | Last modification timestamp |
| `expiresAt` | Expiry timestamp, see [Data expiry (TTL)](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#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](#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](#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](https://www.pubnub.com/docs/general/data-sync/events.md) 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](https://www.pubnub.com/docs/general/channels/subscribe.md#wildcard-subscribe).

In [Bob's marketplace](https://www.pubnub.com/docs/general/data-sync/overview.md#running-example-bobs-live-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:

```json
{
  "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](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md).

The relationship endpoints are [Get relationships](https://www.pubnub.com/docs/sdks/rest-api/get-relationships.md), [Get relationship](https://www.pubnub.com/docs/sdks/rest-api/get-relationship.md), [Create relationship](https://www.pubnub.com/docs/sdks/rest-api/create-relationship.md), [Update relationship](https://www.pubnub.com/docs/sdks/rest-api/update-relationship.md), [Patch relationship](https://www.pubnub.com/docs/sdks/rest-api/patch-relationship.md), and [Delete relationship](https://www.pubnub.com/docs/sdks/rest-api/delete-relationship.md).

System fields on every relationship:

| Field | Purpose |
| --- | --- |
| `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](https://www.pubnub.com/docs/general/data-sync/data-operations.md) |
| `createdAt` | Creation timestamp |
| `updatedAt` | Last modification timestamp |
| `expiresAt` | Expiry timestamp, see [Data expiry (TTL)](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#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.

:::note 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](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#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)](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#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](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md) 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:

* An **entity class** types an [entity](#entities), a stored object. Entity classes support inheritance, so a class you define can extend a Global class and add its own fields (see [Schemas and validation](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md)). Read one back with [Get entity class by ID](https://www.pubnub.com/docs/admin-api/get-entity-class-by-id.md).
* A **relationship class** types a [relationship](#relationships), a link between two entities. Relationship classes declare a cardinality, can restrict which entity class is allowed on each side, and don't support inheritance. Read one back with [Get relationship class by name and version](https://www.pubnub.com/docs/admin-api/get-relationship-class-by-name-and-version.md).

### 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](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), [read](https://www.pubnub.com/docs/admin-api/get-entity-class-by-id.md), [create](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md), [replace](https://www.pubnub.com/docs/admin-api/update-entity-class-with-complete-resource-replacement.md), and [delete](https://www.pubnub.com/docs/admin-api/delete-entity-class-by-id.md), and so do relationship classes, [list](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md), [read](https://www.pubnub.com/docs/admin-api/get-relationship-class-by-name-and-version.md), [create](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md), [replace](https://www.pubnub.com/docs/admin-api/update-relationship-class-with-complete-resource-replacement.md), and [delete](https://www.pubnub.com/docs/admin-api/delete-relationship-class-by-name-and-version.md). Partial entity-class updates aren't implemented; replace the complete class version instead.

###### Classes declare the fields they guard

Entity and relationship classes can declare property definitions that control validation, filtering, and field-level access. See [Schemas and validation](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md).

## 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](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md) for the operational details.

###### Users, channels, and memberships build on this model

The built-in classes follow the same entity and relationship rules described here, with some operational specifics of their own. See [Users, channels, and memberships](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md).

## 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](https://www.pubnub.com/docs/general/data-sync/data-operations.md)) and how you declare fields at the class level (see [Schemas and validation](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md)).

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`.

:::tip 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](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md).
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](https://www.pubnub.com/docs/general/data-sync/data-operations.md#built-in-fields).
:::

###### Changes can publish real-time events

Creating, updating, or deleting an entity or a relationship can publish a DataSync event on the channels the change affects, which aren't always just the changed object's own channel. See [Events](https://www.pubnub.com/docs/general/data-sync/events.md#where-events-are-published) for the full routing model.

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

###### JavaScript

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

###### C#

```csharp
PNResult<PNDataSyncEntityResult> response = await pubnub.DataSync.CreateEntity(new CreateEntityParameters
{
    Id = "product-sneaker-42",
    EntityClass = "product",
    EntityClassVersion = 1,
    Payload = new Dictionary<string, object>
    {
        { "name", "Retro Sneaker" },
        { "price", 89.99 },
        { "stock", 12 },
    },
});
```

Refer to [Create entity (JavaScript)](https://www.pubnub.com/docs/sdks/javascript/api-reference/data-sync.md#create-entity), [Create entity (C#)](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/data-sync.md#create-entity), and [Create entity (REST)](https://www.pubnub.com/docs/sdks/rest-api/create-entity.md) for the full parameter reference, and to [Creating objects](https://www.pubnub.com/docs/general/data-sync/data-operations.md#creating-objects) for what each object kind requires.

## Terms in this document

* **Channel** - A pathway for sending and receiving messages between devices, created automatically when you first use it, that can handle any number of users and messages for different communication needs, like 1-1 text chats, group conversations, and other data streaming.
* **Class** - A versioned type definition (name plus integer version) for entities or relationships in DataSync. Classes carry property definitions that declare which payload fields are validated, filterable, and scoped by projections.
* **Membership** - A relationship in DataSync that links a channel to a user, using the built-in many-to-many Membership class.
* **User** - An individual or entity that interacts with a system, application, or service. In PubNub, a user typically refers to someone who sends or receives messages through the platform, identified by a unique user ID or username.

Last updated at: 2026-09-08T16:52:30.000Z
