---
source_url: https://www.pubnub.com/docs/data-storage/structured-data/data-operations
title: DataSync data operations
updated_at: 2026-09-30T07:20:08.000Z
---

# DataSync data operations

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

Every DataSync resource, entities, relationships, users, channels, and memberships, supports the same operation set: list, get, create, replace, partial update, and delete. All of these operations require [Access Manager](https://www.pubnub.com/docs/security/access-control/overview.md) authorization. Refer to [DataSync access control](https://www.pubnub.com/docs/data-storage/structured-data/access-control.md) for how that works.

Every operation maps to a Core REST API endpoint:

|  | Entity | Relationship | User | Channel | Membership |
| --- | --- | --- | --- | --- | --- |
| List | `Get entities` | `Get relationships` | `Get users` | `Get channels` | `Get memberships` |
| Get | `Get entity` | `Get relationship` | `Get user` | `Get channel` | `Get membership` |
| Create | `Create entity` | `Create relationship` | `Create user` | `Create channel` | `Create membership` |
| Replace | `Update entity` | `Update relationship` | `Update user` | `Update channel` | `Update membership` |
| Partial update | `Patch entity` | `Patch relationship` | `Patch user` | `Patch channel` | `Patch membership` |
| Delete | `Delete entity` | `Delete relationship` | `Delete user` | `Delete channel` | `Delete membership` |

For code examples, refer to [Create entities and relationships](https://www.pubnub.com/docs/data-storage/structured-data/create-entities-and-relationships.md) and [Query DataSync](https://www.pubnub.com/docs/data-storage/structured-data/query-structured-data.md).

## Creating objects

Every create names the class the new object is an instance of. What else you must supply depends on the object kind:

| Creating | You must supply |
| --- | --- |
| An entity | The class name and class version |
| A relationship | Both linked entity IDs, the class name, and the class version |
| A user | The class version. The class name defaults to `User` |
| A channel | The class version. The class name defaults to `Channel` |
| A membership | `channelId`, `userId`, and the class version |

You can optionally supply your own `id`, a `status`, and the payload. If you don't supply an `id`, the server generates one. Supplying an `id` that already exists is rejected with a `409 Conflict`.

## Reading objects

Getting an object by ID returns its system fields and its payload. Projection filtering applies to `payload` and `status` only: the remaining system fields are always returned in full. Refer to [Projections](https://www.pubnub.com/docs/data-storage/structured-data/projections.md).

Expired objects are not returned by reads. Eventually consistent search (`filter`) can return an object whose `expiresAt` has passed, so check `expiresAt` on results from that tier.

## Updating objects

DataSync supports two ways to update an object: replace and partial update.

### Replace

Replace is a `PUT`. It sends the new state for every payload field visible within the token's resolved projection, including the class version. Payload fields outside that projection are left untouched. For a class with no custom projections, replace behaves exactly like sending the full object.

Two things to watch in a replace body:

* The class version isn't a passive echo. Sending a different existing version of the same class re-points the object at that version. A version whose base Global class differs from the object's own is rejected with a `400`.
* `status` is protected by its declared projections. If the resolved projection includes `/status`, omitting `status` from a replace body sets it to null. If the resolved projection excludes `/status`, changing it is rejected with a `403`.

### Partial update

Partial update is a `PATCH`. It uses JSON Patch (RFC 6902) with `add`, `remove`, `replace`, `move`, `copy`, and `test` operations, addressed by JSON Pointer paths into the object. A pointer starts at the object, where `payload` is one field alongside the system fields, so the price on a product is `/payload/price`.

A partial update can only address three paths on the object:

* `/status`
* `/entityClassVersion`, or `/relationshipClassVersion` for relationships and memberships
* `/payload`, or anything beneath it

Any other pointer is rejected with a `400 Bad Request`. The `test` operation is the exception: it can target any pointer without modifying the document, which makes it a way to assert `/eTag` inline as part of the patch.

## Optimistic concurrency with ETags

Every entity and relationship carries an `eTag` that changes on every write. `If-Match` is an optional request header on every write endpoint. Omit it and the write applies unconditionally. Send the `eTag` you last read as `If-Match` to make the write conditional, or `If-Match: *` to require only that the object still exists. If the value no longer matches the object's current `eTag`, the write is rejected with a `412 Precondition Failed`.

```mermaid
flowchart LR
    READ["Read<br/>save eTag"]
    WRITE["Write<br/>If-Match"]
    SUCCESS["200 OK<br/>new eTag"]
    CONFLICT["412<br/>re-read"]

    READ --> WRITE
    WRITE -->|"matches"| SUCCESS
    WRITE -->|"stale"| CONFLICT
    CONFLICT -->|"retry"| READ

    class CONFLICT emphasis
```

The pattern for safe concurrent writes:

1. Read the object and note its `eTag`.
2. Modify the fields you want to change.
3. Write with `If-Match: <eTag>`. If the `eTag` still matches, the write succeeds with `200 OK` and returns the new `eTag`.
4. If the response is `412`, re-read and retry.

If you're also subscribed to an object's events, use an incoming event as a signal to refresh your copy and its `eTag` before writing, rather than writing blindly.

## Deleting objects

Delete removes an object immediately. Like other writes, it respects `If-Match`, so a delete against a stale `eTag` is rejected with a `412`. Deleting an entity also deletes every relationship linked to it, and each cascaded relationship delete publishes an event of its own.

A successful delete returns `200 OK` with an empty body. Deleting an ID that doesn't exist returns a `404`, so delete isn't idempotent.

Objects also expire automatically based on their class TTL. `expiresAt` is set once at creation and is not refreshed by updates. Refer to [Schemas: data expiry (TTL)](https://www.pubnub.com/docs/data-storage/structured-data/schemas.md#data-expiry-ttl).

## Filtering

DataSync offers two mutually exclusive filtering parameters: `filter_fast` for strongly consistent queries and `filter` for eventually consistent queries. Supplying both in one request is rejected with a `400`.

|  | Strongly consistent (`filter_fast`) | Eventually consistent (`filter`) |
| --- | --- | --- |
| Eligible properties | Declared with filtering mode `simple` or `full` | Declared with filtering mode `full` |
| Storage | Same store used for writes | A separate search store |
| Consistency | Strongly consistent; reflects completed writes | Eventually consistent; can briefly lag recent writes |
| Predicate count | Maximum 10 by default, configurable per keyset | No predicate limit is enforced |

Both parameters share the same expression language. Filter expressions use comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`), pattern operators (`LIKE`, `SLIKE`, `ILIKE`), and logical operators (`&&`, `||`, `!`). In filter and sort expressions, you always use a property's `name`, not its `path`.

Four fields every object carries natively are filterable and sortable without being declared on the class:

| Field | Filters as |
| --- | --- |
| `id` | `string` |
| `createdAt` | `datetime` |
| `updatedAt` | `datetime` |
| `status` | `string` |

:::warning Declare filterable properties first
Beyond the four built-in fields, filtering only works on properties you've declared on the class, and only for data written after the property was declared. Data written earlier isn't retroactively indexed.
:::

Listing generic entities requires an `entity_class`, and listing generic relationships requires a `relationship_class`. There's no way to list every entity on a keyset regardless of class.

## Sorting and pagination

List endpoints accept a `sort` parameter: a comma-separated list of property names, each optionally followed by `:desc` for descending order. Only declared properties are sortable, and which properties are sortable depends on the filtering tier: `filter_fast` supports `simple` and `full` properties, while `filter` supports `full` only.

List endpoints use cursor-based pagination. Page size ranges from 1 to 100, with a default of 20. Pagination is forward-only. To page forward, pass the response's `next_cursor` back as `cursor` on the next call, and stop when `has_next` is `false`.

Refer to [Query DataSync](https://www.pubnub.com/docs/data-storage/structured-data/query-structured-data.md) for code examples.

Last updated at: 2026-09-30T07:20:08.000Z
