---
source_url: https://www.pubnub.com/docs/sdks/swift/api-reference/data-sync
title: DataSync API for Swift SDK
updated_at: 2026-10-13T07:06:43.000Z
sdk_name: PubNub Swift SDK
sdk_version: 11.0.0
---

# DataSync API for Swift SDK

PubNub Swift SDK, use the latest version: 11.0.0

Install:

```bash
Add PubNub via Swift Package Manager or CocoaPods@11.0.0
```

> For AI agents: documentation index at https://www.pubnub.com/llms-full.txt

[DataSync](https://www.pubnub.com/docs/data-storage/structured-data/overview.md) 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](https://www.pubnub.com/docs/sdks/swift/api-reference/objects.md).

:::note 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.
An **SDK entity** is a local client-side handle such as `pubnub.channel("room-1")`. It carries no stored state of its own, it just gives you a scoped surface for subscribing and calling APIs. Refer to [SDK entities](https://www.pubnub.com/docs/sdks/swift/api-reference/publish-and-subscribe.md#entities) in Publish and subscribe.
The two meet in the [DataSync SDK entities](https://www.pubnub.com/docs/sdks/swift/api-reference/publish-and-subscribe.md#datasync-sdk-entities): SDK entities whose only job is to subscribe to a DataSync object's real-time updates.
:::

The classes that your objects conform to (their types and schemas) are defined through the Admin API or the [Admin Portal](https://admin.pubnub.com/), not through this SDK:

* Entity classes, which back users, channels, and custom entities: [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). Partial updates aren't implemented; replace the complete class version instead.
* Relationship classes, which back memberships and custom relationships: [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).

`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](https://www.pubnub.com/docs/data-storage/structured-data/projections.md) each field belongs to, and how long the object lives before it expires. Refer to [managing classes](https://www.pubnub.com/docs/data-storage/structured-data/schemas.md#managing-classes) for more information.

Every DataSync method reports through its `completion` closure, which receives a `Result`. A successful call returns the typed object (`PubNubDataSyncUser`, `PubNubDataSyncChannel`, `PubNubDataSyncMembership`, `PubNubDataSyncEntity`, or `PubNubDataSyncRelationship`). A list method returns a tuple holding the array of objects and an optional `PubNubDataSyncPage`, and the `remove*` methods return `Void`. The `Response` blocks on this page show the JSON the service sends, which the SDK decodes for you. It surfaces neither the HTTP `status` nor the HATEOAS `links`. A stored object carries its free-form `status` and its `expiresAt` auto-deletion timestamp (ISO 8601) whenever those are set on it. You pass `payload` as any `JSONCodable` value, such as a `Codable` struct or a dictionary, and decode it back with `payload?.decode(_:)`.

### Authorization

Every DataSync request must be [authorized](https://www.pubnub.com/docs/data-storage/structured-data/access-control.md#authorizing-requests) 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`.

The Swift SDK is a client-side SDK, so it doesn't sign requests with a secret key. Never put a secret key in a client-side application. Give the client a token that a server SDK granted, as described in [grant structured data access](https://www.pubnub.com/docs/data-storage/structured-data/grant-structured-data-access.md), and set it with [set(token:)](https://www.pubnub.com/docs/sdks/swift/api-reference/access-manager.md#set-token).

### Pagination

DataSync pagination is **forward-only**. There is no previous-page cursor, and `PubNubDataSyncPage` exposes no `prevCursor` 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 `next` value of type `PubNubDataSyncPage` with `hasNext`, `cursor`, and `limit`. The `next` value is optional, so unwrap it before you read it, for example `next?.cursor`.

To page forward, pass the returned `next.cursor` back as `cursor` on the next call, and stop when `next.hasNext` is `false`. `next.cursor` is `nil` on the last page. Refer to [sorting and pagination](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#sorting-and-pagination) for details.

### Filtering and sorting

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.

:::warning danger
filter
is not the strongly consistent one
The 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:

```swift
filterFast: "price < 100"                               // strongly consistent, single condition
filterFast: "(price < 100 && stock > 0) || price > 500" // strongly consistent, grouped conditions
filter: "name LIKE \"*sneaker*\""                       // eventually consistent, needs filtering mode "full"
filter: "!(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. When you don't pass `classVersion`, the list includes items of every version, but `filter`, `filterFast`, and `sort` can only use properties declared on the latest version of the class. A property that exists only in an older version is rejected with a `400`. Pass `classVersion` to use properties declared on an older version. Refer to [filtering](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#filtering) for the two filtering tiers, and [property definitions](https://www.pubnub.com/docs/data-storage/structured-data/schemas.md#property-definitions) for how properties are declared.

Use `sort` to order results. Pass an array of `PubNub.DataSyncSortField` in priority order, where each one names a `property` and sets `ascending` (default `true`). The SDK joins the array into a comma-separated request value and appends `:desc` to each descending field. A bare name sorts ascending:

```swift
sort: [.init(property: "price", ascending: false)]                           // single field, descending
sort: [.init(property: "type"), .init(property: "price", ascending: false)]  // type ascending, then price descending
sort: [.init(property: "createdAt", ascending: true)]                        // explicit ascending
```

The `+field` and `-field` prefixes are **not** accepted. A leading `-` is read as part of the property name, so `.init(property: "-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 only local checks are an empty resource `id`, an empty `operations` array, an empty `className` (on `createEntity`, `getEntities`, `createRelationship`, and `getRelationships`), an empty `channelId` or `userId` on `createMembership`, and an empty `entityAId` or `entityBId` on `createRelationship`. These fail with a `PubNubError` whose `reason` is `.missingRequiredParameter` before anything is sent.

### Concurrency (ETag)

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](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#optimistic-concurrency-with-etags) for details.

### Partial updates

The `update*` methods apply a partial update using raw JSON Patch (RFC 6902). Pass an array of `PubNubDataSyncPatchOperation`. Each operation is one of the enum cases `add`, `remove`, `replace`, `move`, `copy`, or `test`, with a `path` (a full JSON Pointer, for example `/status` or `/payload/price`) and, depending on the case, a `value` or a `from`.

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](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#partial-update) for details.

Correct patch forms:

```swift
.add(path: "/payload/tags", value: "sale")           // adds a value inside payload.tags
.replace(path: "/payload/price", value: 79.99)       // replaces payload.price
.remove(path: "/payload/tempFlag")                   // removes payload.tempFlag
.move(from: "/payload/oldTag", path: "/payload/tag")
.copy(from: "/payload/price", path: "/payload/msrp")
.test(path: "/status", value: "active")              // fails the patch if the current value doesn't match
```

`move` and `copy` take `from` (the source pointer) instead of `value`. `test` takes a `value` and fails the entire patch if the current value at `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 `operations` array must contain at least one operation. The SDK checks this before it sends anything and fails the call with a `PubNubError` whose `reason` is `.missingRequiredParameter` if the array is empty.

### Expiry (TTL)

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](https://www.pubnub.com/docs/data-storage/structured-data/schemas.md#data-expiry-ttl) for details.

:::note Asynchronous calls and errors
Every DataSync method is asynchronous and delivers its outcome to the `completion` closure. The SDK has no `async` or `await` variant, so wrap a call in a continuation if you need one.
A failed call arrives in the `.failure` case as an `Error` that you cast with `error.pubNubError`. Its `reason` follows the HTTP status, for example `.badRequest`, `.unauthorized`, `.forbidden`, `.resourceNotFound`, `.conflict`, or `.preconditionFailed`, and its `details` hold the messages the service returned.
:::

:::note Requires Access Manager
DataSync requires that the *Access Manager* add-on is enabled for your key in the [Admin Portal](https://admin.pubnub.com/). Read the [support page](https://support.pubnub.com/hc/en-us/articles/360051974791-How-do-I-enable-add-on-features-for-my-keys-) 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](https://www.pubnub.com/docs/data-storage/structured-data/built-in-types.md#users) for the concept.

A user is an entity of the built-in `User` [entity class](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), 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](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) 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)

```swift
func createUser(
    className: String? = nil,
    classVersion: Int,
    classLevel: PubNubDataSyncClassLevel? = nil,
    id: String? = nil,
    status: String? = nil,
    payload: JSONCodable? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncUser, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Optional | `server-generated` | User identifier. Omit to let the server generate a UUID. Max 255 characters. |
| className | String | Optional | `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. |
| classLevel | PubNubDataSyncClassLevel | Optional | `service default` | Class hierarchy level of `className`, either `.global` for a class the service provides or `.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. |
| classVersion | Int | Yes |  | Version of the user class schema. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncUser`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a user, as a type the SDK can encode into a request and decode from a response
struct UserProfile: JSONCodable {
  let fullName: String
  let email: String
  let loginCount: Int
  let isEmailVerified: Bool
}

// Create a user
pubnub.dataSync.createUser(
  classVersion: 1,
  id: "alice",
  status: "active",
  payload: UserProfile(
    fullName: "Alice Summers",
    email: "alice@example.com",
    loginCount: 0,
    isEmailVerified: false
  )
) { result in
  switch result {
  case let .success(user):
    print("The created user: \(user)")
  case let .failure(error):
    print("Create user request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"fullName": "Alice Summers", "email": "alice@example.com", "loginCount": 0, "isEmailVerified": false},
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T09:00:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "AbQdEfGhIjKlMn"
    }
}
```

### Get user

Returns a single user by `id`.

#### Method(s)

```swift
func getUser(
    id: String,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncUser, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | User identifier. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncUser`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a user, as a type the SDK can encode into a request and decode from a response
struct UserProfile: JSONCodable {
  let fullName: String
  let email: String
  let loginCount: Int
  let isEmailVerified: Bool
}

// Retrieve a single user by identifier, and decode its payload into your own type
pubnub.dataSync.getUser(id: "alice") { result in
  switch result {
  case let .success(user):
    print("The user for `\(user.id)`: \(user)")
    print("Its eTag, used for optimistic concurrency: \(user.eTag)")

    do {
      if let profile = try user.payload?.decode(UserProfile.self) {
        print("\(profile.fullName) has logged in \(profile.loginCount) times")
        print("Their email is verified: \(profile.isEmailVerified)")
      } else {
        print("The user has no stored payload")
      }
    } catch {
      print("Could not decode \(user.className) version \(user.classVersion) for user \(user.id): \(error)")
    }
  case let .failure(error):
    print("Get user request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"fullName": "Alice Summers", "email": "alice@example.com", "loginCount": 0, "isEmailVerified": false},
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T09:00:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "AbQdEfGhIjKlMn"
    }
}
```

### Get all users

Returns a paginated list of users. All parameters other than `completion` are optional, so you can call `getUsers` with only a `completion` closure. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#sorting-and-pagination).

#### Method(s)

```swift
func getUsers(
    className: String? = nil,
    classVersion: Int? = nil,
    classLevel: PubNubDataSyncClassLevel? = nil,
    cursor: String? = nil,
    limit: Int? = nil,
    filterFast: String? = nil,
    filter: String? = nil,
    sort: [PubNub.DataSyncSortField] = [],
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<(users: [PubNubDataSyncUser], next: PubNubDataSyncPage?), Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| className | String | Optional | `all user classes` | User class to list. Omit to list the Global `User` class and all of its subclasses. |
| classVersion | Int | Optional | `all versions` | User class version to list. Omit to list users across every version of the class. |
| classLevel | PubNubDataSyncClassLevel | Optional | `service default` | Class hierarchy level of `className`, either `.global` for a class the service provides or `.subKey` for one defined on your keyset. Used to disambiguate a class name defined at both levels. |
| cursor | String | Optional |  | Opaque pagination cursor. Omit for the first page. |
| limit | Int | Optional | `20` | Maximum number of users per page. Max `100`. |
| filterFast | String | Optional |  | 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`. |
| filter | String | Optional |  | 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`. |
| sort | [PubNub.DataSyncSortField] | Optional | `[]` | Order results. Each `PubNub.DataSyncSortField` names a `property` and sets `ascending` (default `true`), for example `.init(property: "createdAt", ascending: false)`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a tuple of the `users` array and the optional `next` page. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve a page of users
pubnub.dataSync.getUsers(limit: 20) { result in
  switch result {
  case let .success((users, next)):
    print("The users: \(users)")
    print("The next page used for pagination: \(String(describing: next))")
  case let .failure(error):
    print("Get users request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "alice",
            "entityClass": "User",
            "entityClassVersion": 1,
            "entityClassLevel": "Global",
            "status": "active",
            "payload": {"fullName": "Alice Summers", "email": "alice@example.com", "loginCount": 0, "isEmailVerified": false},
            "createdAt": "2026-07-13T09:00:00.000Z",
            "updatedAt": "2026-07-13T09:00:00.000Z",
            "expiresAt": "2026-08-13T00:00:00.000Z",
            "eTag": "AbQdEfGhIjKlMn"
        }
    ],
    "meta": {
        "next_cursor": "b2Zmc2V0PTIw",
        "has_next": true,
        "limit": 20
    }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter strongly consistent data, so a user you just wrote is matched
pubnub.dataSync.getUsers(
  limit: 20,
  filterFast: "isEmailVerified == true && loginCount > 5"
) { result in
  switch result {
  case let .success((users, _)):
    print("The matching users: \(users)")
  case let .failure(error):
    print("Get users request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter eventually consistent data with the full expression language
pubnub.dataSync.getUsers(
  limit: 20,
  filter: "fullName LIKE \"*Summers*\""
) { result in
  switch result {
  case let .success((users, _)):
    print("The matching users: \(users)")
  case let .failure(error):
    print("Get users request failed with error: \(error.localizedDescription)")
  }
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `next.cursor` from the response and pass it back as `cursor` on the next call. Stop when `next.hasNext` is `false`.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Page through every user, passing the cursor of each page into the next request
func fetchUsers(cursor: String? = nil) {
  pubnub.dataSync.getUsers(
    cursor: cursor,
    limit: 20
  ) { result in
    switch result {
    case let .success((users, next)):
      print("A page of users: \(users)")

      // Stop when the response has no next page
      if let next = next, next.hasNext {
        fetchUsers(cursor: next.cursor)
      }
    case let .failure(error):
      print("Get users request failed with error: \(error.localizedDescription)")
    }
  }
}

fetchUsers()
```

### Set user

Replaces a user in full (PUT). Send the complete set of fields, any field you omit is cleared. `className` and `classLevel` are immutable after creation and cannot be sent. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`. For a partial update, use [Update user](#update-user).

#### Method(s)

```swift
func setUser(
    id: String,
    classVersion: Int,
    status: String? = nil,
    payload: JSONCodable? = nil,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncUser, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | User identifier. |
| classVersion | Int | Yes |  | Version of the user class schema. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncUser`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a user, as a type the SDK can encode into a request and decode from a response
struct UserProfile: JSONCodable {
  let fullName: String
  let email: String
  let loginCount: Int
  let isEmailVerified: Bool
}

// Replace every mutable field. Every field to keep must be sent back; an omitted field is cleared
pubnub.dataSync.setUser(
  id: "alice",
  classVersion: 1,
  status: "active",
  payload: UserProfile(
    fullName: "Alice R. Summers",
    email: "alice.summers@example.com",
    loginCount: 12,
    isEmailVerified: true
  ),
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(user):
    print("The set user: \(user)")
  case let .failure(error):
    print("Set user request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"fullName": "Alice R. Summers", "email": "alice.summers@example.com", "loginCount": 12, "isEmailVerified": true},
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T10:15:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "CdEfGhIjKlMnOp"
    }
}
```

### Update user

Applies a partial update to a user with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#partial-update) for the operation model.

#### Method(s)

```swift
func updateUser(
    id: String,
    operations: [PubNubDataSyncPatchOperation],
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncUser, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | User identifier. |
| operations | [PubNubDataSyncPatchOperation] | Yes |  | One or more JSON Patch operations. Must contain at least one item. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncUser`. Pass `nil` to ignore the result. |

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

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Change selected fields, leaving every unaddressed field untouched
pubnub.dataSync.updateUser(
  id: "alice",
  operations: [
    .replace(path: "/payload/email", value: "alice.summers@example.com"),
    .replace(path: "/payload/isEmailVerified", value: true),
    .replace(path: "/payload/loginCount", value: 13),
    .add(path: "/payload/nickname", value: "Ali")
  ],
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(user):
    print("The updated user: \(user)")
  case let .failure(error):
    print("Update user request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"fullName": "Alice R. Summers", "email": "alice.summers@example.com", "loginCount": 13, "isEmailVerified": true, "nickname": "Ali"},
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T10:20:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "EfGhIjKlMnOpQr"
    }
}
```

### Remove user

Deletes a user by `id`.

#### Method(s)

```swift
func removeUser(
    id: String,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<Void, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | User identifier. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds `Void`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Remove a user
pubnub.dataSync.removeUser(
  id: "alice",
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case .success:
    print("The user was removed")
  case let .failure(error):
    print("Remove user request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200
}
```

## 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](https://www.pubnub.com/docs/data-storage/structured-data/built-in-types.md#channels) for the concept.

A channel is an entity of the built-in `Channel` [entity class](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), which the service provides at the `Global` class level. Subclass it with [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) 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)

```swift
func createChannel(
    className: String? = nil,
    classVersion: Int,
    classLevel: PubNubDataSyncClassLevel? = nil,
    id: String? = nil,
    status: String? = nil,
    payload: JSONCodable? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncChannel, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Optional | `server-generated` | Channel identifier. Omit to let the server generate a UUID. Max 255 characters. |
| className | String | Optional | `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. |
| classLevel | PubNubDataSyncClassLevel | Optional | `service default` | Class hierarchy level of `className`, either `.global` for a class the service provides or `.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. |
| classVersion | Int | Yes |  | Version of the channel class schema. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncChannel`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a channel, as a type the SDK can encode into a request and decode from a response
struct ChannelDetails: JSONCodable {
  let name: String
  let topic: String
  let retentionDays: Int
  let isPrivate: Bool
}

// Create a channel
pubnub.dataSync.createChannel(
  classVersion: 1,
  id: "general",
  status: "active",
  payload: ChannelDetails(
    name: "General",
    topic: "Company-wide announcements",
    retentionDays: 30,
    isPrivate: false
  )
) { result in
  switch result {
  case let .success(channel):
    print("The created channel: \(channel)")
  case let .failure(error):
    print("Create channel request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"name": "General", "topic": "Company-wide announcements", "retentionDays": 30, "isPrivate": false},
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T09:05:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "GhIjKlMnOpQrSt"
    }
}
```

### Get channel

Returns a single channel by `id`.

#### Method(s)

```swift
func getChannel(
    id: String,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncChannel, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Channel identifier. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncChannel`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a channel, as a type the SDK can encode into a request and decode from a response
struct ChannelDetails: JSONCodable {
  let name: String
  let topic: String
  let retentionDays: Int
  let isPrivate: Bool
}

// Retrieve a single channel by identifier, and decode its payload into your own type
pubnub.dataSync.getChannel(id: "general") { result in
  switch result {
  case let .success(channel):
    print("The channel for `\(channel.id)`: \(channel)")
    print("Its eTag, used for optimistic concurrency: \(channel.eTag)")

    do {
      if let details = try channel.payload?.decode(ChannelDetails.self) {
        print("\(details.name) discusses \(details.topic)")
        print("It keeps messages for \(details.retentionDays) days, and is private: \(details.isPrivate)")
      } else {
        print("The channel has no stored payload")
      }
    } catch {
      print("Could not decode \(channel.className) version \(channel.classVersion) for channel \(channel.id): \(error)")
    }
  case let .failure(error):
    print("Get channel request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"name": "General", "topic": "Company-wide announcements", "retentionDays": 30, "isPrivate": false},
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T09:05:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "GhIjKlMnOpQrSt"
    }
}
```

### Get all channels

Returns a paginated list of channels. All parameters other than `completion` are optional, so you can call `getChannels` with only a `completion` closure. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#sorting-and-pagination).

#### Method(s)

```swift
func getChannels(
    className: String? = nil,
    classVersion: Int? = nil,
    classLevel: PubNubDataSyncClassLevel? = nil,
    cursor: String? = nil,
    limit: Int? = nil,
    filterFast: String? = nil,
    filter: String? = nil,
    sort: [PubNub.DataSyncSortField] = [],
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<(channels: [PubNubDataSyncChannel], next: PubNubDataSyncPage?), Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| className | String | Optional | `all channel classes` | Channel class to list. Omit to list the Global `Channel` class and all of its subclasses. |
| classVersion | Int | Optional | `all versions` | Channel class version to list. Omit to list channels across every version of the class. |
| classLevel | PubNubDataSyncClassLevel | Optional | `service default` | Class hierarchy level of `className`, either `.global` for a class the service provides or `.subKey` for one defined on your keyset. Used to disambiguate a class name defined at both levels. |
| cursor | String | Optional |  | Opaque pagination cursor. Omit for the first page. |
| limit | Int | Optional | `20` | Maximum number of channels per page. Max `100`. |
| filterFast | String | Optional |  | 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`. |
| filter | String | Optional |  | 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`. |
| sort | [PubNub.DataSyncSortField] | Optional | `[]` | Order results. Each `PubNub.DataSyncSortField` names a `property` and sets `ascending` (default `true`). |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a tuple of the `channels` array and the optional `next` page. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve a page of channels
pubnub.dataSync.getChannels(limit: 20) { result in
  switch result {
  case let .success((channels, next)):
    print("The channels: \(channels)")
    print("The next page used for pagination: \(String(describing: next))")
  case let .failure(error):
    print("Get channels request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "general",
            "entityClass": "Channel",
            "entityClassVersion": 1,
            "entityClassLevel": "Global",
            "status": "active",
            "payload": {"name": "General", "topic": "Company-wide announcements", "retentionDays": 30, "isPrivate": false},
            "createdAt": "2026-07-13T09:05:00.000Z",
            "updatedAt": "2026-07-13T09:05:00.000Z",
            "expiresAt": "2026-08-13T00:00:00.000Z",
            "eTag": "GhIjKlMnOpQrSt"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter strongly consistent data, so a channel you just wrote is matched
pubnub.dataSync.getChannels(
  limit: 20,
  filterFast: "isPrivate == false && retentionDays <= 30"
) { result in
  switch result {
  case let .success((channels, _)):
    print("The matching channels: \(channels)")
  case let .failure(error):
    print("Get channels request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter eventually consistent data with the full expression language
pubnub.dataSync.getChannels(
  limit: 20,
  filter: "topic LIKE \"*announcements*\""
) { result in
  switch result {
  case let .success((channels, _)):
    print("The matching channels: \(channels)")
  case let .failure(error):
    print("Get channels request failed with error: \(error.localizedDescription)")
  }
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `next.cursor` from the response and pass it back as `cursor` on the next call. Stop when `next.hasNext` is `false`.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Page through every channel, passing the cursor of each page into the next request
func fetchChannels(cursor: String? = nil) {
  pubnub.dataSync.getChannels(
    cursor: cursor,
    limit: 20
  ) { result in
    switch result {
    case let .success((channels, next)):
      print("A page of channels: \(channels)")

      // Stop when the response has no next page
      if let next = next, next.hasNext {
        fetchChannels(cursor: next.cursor)
      }
    case let .failure(error):
      print("Get channels request failed with error: \(error.localizedDescription)")
    }
  }
}

fetchChannels()
```

### Set channel

Replaces a channel in full (PUT). Send the complete set of fields, any field you omit is cleared. `className` and `classLevel` are immutable after creation and cannot be sent. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`. For a partial update, use [Update channel](#update-channel).

#### Method(s)

```swift
func setChannel(
    id: String,
    classVersion: Int,
    status: String? = nil,
    payload: JSONCodable? = nil,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncChannel, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Channel identifier. |
| classVersion | Int | Yes |  | Version of the channel class schema. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncChannel`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a channel, as a type the SDK can encode into a request and decode from a response
struct ChannelDetails: JSONCodable {
  let name: String
  let topic: String
  let retentionDays: Int
  let isPrivate: Bool
}

// Replace every mutable field. Every field to keep must be sent back; an omitted field is cleared
pubnub.dataSync.setChannel(
  id: "general",
  classVersion: 1,
  status: "active",
  payload: ChannelDetails(
    name: "General",
    topic: "All-hands announcements",
    retentionDays: 90,
    isPrivate: false
  ),
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(channel):
    print("The set channel: \(channel)")
  case let .failure(error):
    print("Set channel request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"name": "General", "topic": "All-hands announcements", "retentionDays": 90, "isPrivate": false},
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T11:00:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "IjKlMnOpQrStUv"
    }
}
```

### Update channel

Applies a partial update to a channel with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#partial-update) for the operation model.

#### Method(s)

```swift
func updateChannel(
    id: String,
    operations: [PubNubDataSyncPatchOperation],
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncChannel, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Channel identifier. |
| operations | [PubNubDataSyncPatchOperation] | Yes |  | One or more JSON Patch operations. Must contain at least one item. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncChannel`. Pass `nil` to ignore the result. |

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

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Change selected fields, leaving every unaddressed field untouched.
// The operations are atomic: if any one fails, the channel is left unchanged
pubnub.dataSync.updateChannel(
  id: "general",
  operations: [
    .replace(path: "/payload/topic", value: "Company announcements"),
    .replace(path: "/payload/retentionDays", value: 365),
    .replace(path: "/payload/isPrivate", value: true),
    .add(path: "/payload/archivedAt", value: "2026-08-11")
  ],
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(channel):
    print("The updated channel: \(channel)")
  case let .failure(error):
    print("Update channel request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "status": "active",
        "payload": {"name": "General", "topic": "Company announcements", "retentionDays": 365, "isPrivate": true, "archivedAt": "2026-08-11"},
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T11:05:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "KlMnOpQrStUvWx"
    }
}
```

### Remove channel

Deletes a channel by `id`.

#### Method(s)

```swift
func removeChannel(
    id: String,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<Void, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Channel identifier. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds `Void`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Remove a channel
pubnub.dataSync.removeChannel(
  id: "general",
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case .success:
    print("The channel was removed")
  case let .failure(error):
    print("Remove channel request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200
}
```

## Memberships

A membership links a user to a channel and carries its own `payload`. In the running example, Alice is a member of `general` with the payload `{ "role": "moderator" }`. Refer to [memberships](https://www.pubnub.com/docs/data-storage/structured-data/built-in-types.md#memberships) for the concept.

A membership is a relationship of the built-in Global `Membership` [relationship class](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md), 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](https://www.pubnub.com/docs/data-storage/structured-data/schemas.md#relationship-class-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` and `channelId` pair must be unique for the membership class. A membership that duplicates an existing pair is rejected with a `409`.

#### Method(s)

```swift
func createMembership(
    channelId: String,
    userId: String,
    classVersion: Int,
    id: String? = nil,
    status: String? = nil,
    payload: JSONCodable? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncMembership, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channelId | String | Yes |  | Identifier of the channel in the membership. |
| userId | String | Yes |  | Identifier of the user in the membership. |
| id | String | Optional | `server-generated` | Membership identifier. Omit to let the server generate a UUID. Max 255 characters. |
| classVersion | Int | Yes |  | Version of the membership class schema. Sent to the service as `relationshipClassVersion`. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncMembership`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a membership, as a type the SDK can encode into a request and decode from a response
struct MembershipDetails: JSONCodable {
  let role: String
  let unreadCount: Int
  let isMuted: Bool
}

// Add a user to a channel
pubnub.dataSync.createMembership(
  channelId: "general",
  userId: "alice",
  classVersion: 1,
  id: "general-alice",
  status: "active",
  payload: MembershipDetails(role: "moderator", unreadCount: 0, isMuted: false)
) { result in
  switch result {
  case let .success(membership):
    print("The created membership: \(membership)")
  case let .failure(error):
    print("Create membership request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general-alice",
        "channelId": "general",
        "userId": "alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "status": "active",
        "payload": {"role": "moderator", "unreadCount": 0, "isMuted": false},
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T09:10:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "MnOpQrStUvWxYz"
    }
}
```

### Get membership

Returns a single membership by `id`.

#### Method(s)

```swift
func getMembership(
    id: String,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncMembership, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Membership identifier. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncMembership`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a membership, as a type the SDK can encode into a request and decode from a response
struct MembershipDetails: JSONCodable {
  let role: String
  let unreadCount: Int
  let isMuted: Bool
}

// Retrieve a single membership by identifier, and decode its payload into your own type
pubnub.dataSync.getMembership(id: "general-alice") { result in
  switch result {
  case let .success(membership):
    print("The membership for `\(membership.id)`: \(membership)")
    print("It joins channel `\(membership.channelId)` and user `\(membership.userId)`")
    print("Its eTag, used for optimistic concurrency: \(membership.eTag)")

    do {
      if let details = try membership.payload?.decode(MembershipDetails.self) {
        print("The member is a \(details.role) with \(details.unreadCount) unread messages")
        print("They muted the channel: \(details.isMuted)")
      } else {
        print("The membership has no stored payload")
      }
    } catch {
      print("Could not decode \(membership.className) version \(membership.classVersion)")
    }
  case let .failure(error):
    print("Get membership request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general-alice",
        "channelId": "general",
        "userId": "alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "status": "active",
        "payload": {"role": "moderator", "unreadCount": 0, "isMuted": false},
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T09:10:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "MnOpQrStUvWxYz"
    }
}
```

### Get all memberships

Returns a paginated list of memberships. All parameters other than `completion` are optional, so you can call `getMemberships` with only a `completion` closure. `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](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#sorting-and-pagination).

#### Method(s)

```swift
func getMemberships(
    channelId: String? = nil,
    userId: String? = nil,
    classVersion: Int? = nil,
    cursor: String? = nil,
    limit: Int? = nil,
    filterFast: String? = nil,
    filter: String? = nil,
    sort: [PubNub.DataSyncSortField] = [],
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<(memberships: [PubNubDataSyncMembership], next: PubNubDataSyncPage?), Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channelId | String | Optional |  | List only memberships for this channel. |
| userId | String | Optional |  | List only memberships for this user. |
| classVersion | Int | Optional | `all versions` | Membership class version to list. Omit to list memberships across every version of the class. |
| cursor | String | Optional |  | Opaque pagination cursor. Omit for the first page. |
| limit | Int | Optional | `20` | Maximum number of memberships per page. Max `100`. |
| filterFast | String | Optional |  | 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`. |
| filter | String | Optional |  | 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`. |
| sort | [PubNub.DataSyncSortField] | Optional | `[]` | Order results. Each `PubNub.DataSyncSortField` names a `property` and sets `ascending` (default `true`). |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a tuple of the `memberships` array and the optional `next` page. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve the memberships of a channel
pubnub.dataSync.getMemberships(channelId: "general", limit: 20) { result in
  switch result {
  case let .success((memberships, next)):
    print("The memberships: \(memberships)")
    print("The next page used for pagination: \(String(describing: next))")
  case let .failure(error):
    print("Get memberships request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "general-alice",
            "channelId": "general",
            "userId": "alice",
            "relationshipClass": "Membership",
            "relationshipClassVersion": 1,
            "status": "active",
            "payload": {"role": "moderator", "unreadCount": 0, "isMuted": false},
            "createdAt": "2026-07-13T09:10:00.000Z",
            "updatedAt": "2026-07-13T09:10:00.000Z",
            "expiresAt": "2026-08-13T00:00:00.000Z",
            "eTag": "MnOpQrStUvWxYz"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### List a user's memberships with userId

Pass `userId` instead of `channelId` to list the memberships of a user rather than the members of a channel.

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve the channels a user belongs to
pubnub.dataSync.getMemberships(userId: "alice", limit: 20) { result in
  switch result {
  case let .success((memberships, _)):
    print("The channels `alice` belongs to: \(memberships.map { $0.channelId })")
  case let .failure(error):
    print("Get memberships request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter strongly consistent data, so a membership you just wrote is matched
pubnub.dataSync.getMemberships(
  channelId: "general",
  limit: 20,
  filterFast: "role == \"moderator\""
) { result in
  switch result {
  case let .success((memberships, _)):
    print("The matching memberships: \(memberships)")
  case let .failure(error):
    print("Get memberships request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter eventually consistent data with the full expression language
pubnub.dataSync.getMemberships(
  channelId: "general",
  limit: 20,
  filter: "role LIKE \"*mod*\""
) { result in
  switch result {
  case let .success((memberships, _)):
    print("The matching memberships: \(memberships)")
  case let .failure(error):
    print("Get memberships request failed with error: \(error.localizedDescription)")
  }
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `next.cursor` from the response and pass it back as `cursor` on the next call. Stop when `next.hasNext` is `false`.

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Page through every membership, passing the cursor of each page into the next request
func fetchMemberships(cursor: String? = nil) {
  pubnub.dataSync.getMemberships(
    channelId: "general",
    cursor: cursor,
    limit: 20
  ) { result in
    switch result {
    case let .success((memberships, next)):
      print("A page of memberships: \(memberships)")

      // Stop when the response has no next page
      if let next = next, next.hasNext {
        fetchMemberships(cursor: next.cursor)
      }
    case let .failure(error):
      print("Get memberships request failed with error: \(error.localizedDescription)")
    }
  }
}

fetchMemberships()
```

### Set membership

Replaces a membership in full (PUT). Send the complete set of fields, any field you omit is cleared. The linked user and channel are immutable, so `setMembership` doesn't take `channelId` or `userId`, there is nothing to resend. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`. For a partial update, use [Update membership](#update-membership).

#### Method(s)

```swift
func setMembership(
    id: String,
    classVersion: Int,
    status: String? = nil,
    payload: JSONCodable? = nil,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncMembership, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Membership identifier. |
| classVersion | Int | Yes |  | Version of the membership class schema. Sent to the service as `relationshipClassVersion`. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncMembership`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a membership, as a type the SDK can encode into a request and decode from a response
struct MembershipDetails: JSONCodable {
  let role: String
  let unreadCount: Int
  let isMuted: Bool
}

// Replace every mutable field. Every field to keep must be sent back; an omitted field is cleared
pubnub.dataSync.setMembership(
  id: "general-alice",
  classVersion: 1,
  status: "active",
  payload: MembershipDetails(role: "member", unreadCount: 7, isMuted: true),
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(membership):
    print("The set membership: \(membership)")
  case let .failure(error):
    print("Set membership request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general-alice",
        "channelId": "general",
        "userId": "alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "status": "active",
        "payload": {"role": "member", "unreadCount": 7, "isMuted": true},
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T12:00:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "OpQrStUvWxYzAb"
    }
}
```

### Update membership

Applies a partial update to a membership with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#partial-update) for the operation model.

#### Method(s)

```swift
func updateMembership(
    id: String,
    operations: [PubNubDataSyncPatchOperation],
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncMembership, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Membership identifier. |
| operations | [PubNubDataSyncPatchOperation] | Yes |  | One or more JSON Patch operations. Must contain at least one item. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncMembership`. Pass `nil` to ignore the result. |

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

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Change selected fields, leaving every unaddressed field untouched.
// A `Date` value is sent as an ISO 8601 string
pubnub.dataSync.updateMembership(
  id: "general-alice",
  operations: [
    .replace(path: "/payload/role", value: "admin"),
    .replace(path: "/payload/unreadCount", value: 0),
    .replace(path: "/payload/isMuted", value: false),
    .add(path: "/payload/lastReadAt", value: Date())
  ],
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(membership):
    print("The updated membership: \(membership)")
  case let .failure(error):
    print("Update membership request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "general-alice",
        "channelId": "general",
        "userId": "alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "status": "active",
        "payload": {"role": "admin", "unreadCount": 0, "isMuted": false, "lastReadAt": "2026-07-13T12:05:00.000Z"},
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T12:05:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "QrStUvWxYzAbCd"
    }
}
```

### Remove membership

Deletes a membership by `id`.

#### Method(s)

```swift
func removeMembership(
    id: String,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<Void, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Membership identifier. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds `Void`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK
import Foundation

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Remove a user from a channel
pubnub.dataSync.removeMembership(
  id: "general-alice",
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case .success:
    print("The membership was removed")
  case let .failure(error):
    print("Remove membership request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200
}
```

## Entities

Entities are instances of the custom [entity classes](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md) you define on your keyset. In the running example, `course` is a class and `course-swift-basics` is an instance. Use [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) to declare a class, and [Get entity class by ID](https://www.pubnub.com/docs/admin-api/get-entity-class-by-id.md) 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(className: "course")` returns every `course` plus every instance of a class that extends `course`.

### Create entity

Creates an entity of a given class and version.

#### Method(s)

```swift
func createEntity(
    className: String,
    classVersion: Int,
    classLevel: PubNubDataSyncClassLevel? = nil,
    id: String? = nil,
    status: String? = nil,
    payload: JSONCodable? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncEntity, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Optional | `server-generated` | Entity identifier. Omit to let the server generate a UUID. Max 255 characters. |
| className | String | Yes |  | Name of the entity class this instance belongs to. Set at creation and immutable afterward. |
| classLevel | PubNubDataSyncClassLevel | Optional | `service default` | Class hierarchy level of `className`, either `.global` for a class the service provides or `.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. |
| classVersion | Int | Yes |  | Version of the entity class schema. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncEntity`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a `course` entity, as a type the SDK can encode into a request and decode from a response
struct CourseDetails: JSONCodable {
  let title: String
  let lessonCount: Int
  let isPublished: Bool
  let summary: String?
}

// Create an entity of your own class
pubnub.dataSync.createEntity(
  className: "course",
  classVersion: 1,
  id: "course-swift-basics",
  status: "draft",
  payload: CourseDetails(
    title: "Swift Basics",
    lessonCount: 24,
    isPublished: false,
    summary: nil
  )
) { result in
  switch result {
  case let .success(entity):
    print("The created entity: \(entity)")
  case let .failure(error):
    print("Create entity request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "course-swift-basics",
        "entityClass": "course",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "status": "draft",
        "payload": {"title": "Swift Basics", "lessonCount": 24, "isPublished": false},
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T09:15:00.000Z",
        "expiresAt": "2026-08-14T00:00:00.000Z",
        "eTag": "StUvWxYzAbCdEf"
    }
}
```

### Get entity

Returns a single entity by `id`.

#### Method(s)

```swift
func getEntity(
    id: String,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncEntity, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Entity identifier. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncEntity`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a `course` entity, as a type the SDK can encode into a request and decode from a response
struct CourseDetails: JSONCodable {
  let title: String
  let lessonCount: Int
  let isPublished: Bool
  let summary: String?
}

// Retrieve a single entity by identifier, and decode its payload into your own type
pubnub.dataSync.getEntity(id: "course-swift-basics") { result in
  switch result {
  case let .success(entity):
    print("The entity for `\(entity.id)`: \(entity)")
    print("It belongs to class `\(entity.className)` at level \(entity.classLevel.stringValue)")
    print("Its eTag, used for optimistic concurrency: \(entity.eTag)")

    do {
      if let details = try entity.payload?.decode(CourseDetails.self) {
        print("\(details.title) has \(details.lessonCount) lessons")
        print("It is published: \(details.isPublished)")
        print("Its summary: \(details.summary ?? "not written yet")")
      } else {
        print("The entity has no stored payload")
      }
    } catch {
      print("Could not decode \(entity.className) version \(entity.classVersion) for entity \(entity.id): \(error)")
    }
  case let .failure(error):
    print("Get entity request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "course-swift-basics",
        "entityClass": "course",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "status": "draft",
        "payload": {"title": "Swift Basics", "lessonCount": 24, "isPublished": false},
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T09:15:00.000Z",
        "expiresAt": "2026-08-14T00:00:00.000Z",
        "eTag": "StUvWxYzAbCdEf"
    }
}
```

### 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](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#sorting-and-pagination).

#### Method(s)

```swift
func getEntities(
    className: String,
    classVersion: Int? = nil,
    classLevel: PubNubDataSyncClassLevel? = nil,
    cursor: String? = nil,
    limit: Int? = nil,
    filterFast: String? = nil,
    filter: String? = nil,
    sort: [PubNub.DataSyncSortField] = [],
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<(entities: [PubNubDataSyncEntity], next: PubNubDataSyncPage?), Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| className | String | Yes |  | Name of the entity class to list. |
| classVersion | Int | Optional | `all versions` | Entity class version to list. Omit to list entities across every version of the class. |
| classLevel | PubNubDataSyncClassLevel | Optional | `service default` | Class hierarchy level of `className`, either `.global` for a class the service provides or `.subKey` for one defined on your keyset. Used to disambiguate a class name defined at both levels. |
| cursor | String | Optional |  | Opaque pagination cursor. Omit for the first page. |
| limit | Int | Optional | `20` | Maximum number of entities per page. Max `100`. |
| filterFast | String | Optional |  | 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`. |
| filter | String | Optional |  | 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`. |
| sort | [PubNub.DataSyncSortField] | Optional | `[]` | Order results. Each `PubNub.DataSyncSortField` names a `property` and sets `ascending` (default `true`). |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a tuple of the `entities` array and the optional `next` page. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve a page of entities of your own class
pubnub.dataSync.getEntities(className: "course", limit: 20) { result in
  switch result {
  case let .success((entities, next)):
    print("The entities: \(entities)")
    print("The next page used for pagination: \(String(describing: next))")
  case let .failure(error):
    print("Get entities request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "course-swift-basics",
            "entityClass": "course",
            "entityClassVersion": 1,
            "entityClassLevel": "SubKey",
            "status": "draft",
            "payload": {"title": "Swift Basics", "lessonCount": 24, "isPublished": false},
            "createdAt": "2026-07-13T09:15:00.000Z",
            "updatedAt": "2026-07-13T09:15:00.000Z",
            "expiresAt": "2026-08-14T00:00:00.000Z",
            "eTag": "StUvWxYzAbCdEf"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter strongly consistent data, so a entity you just wrote is matched
pubnub.dataSync.getEntities(
  className: "course",
  limit: 20,
  filterFast: "isPublished == true && lessonCount > 20"
) { result in
  switch result {
  case let .success((entities, _)):
    print("The matching entities: \(entities)")
  case let .failure(error):
    print("Get entities request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter eventually consistent data with the full expression language
pubnub.dataSync.getEntities(
  className: "course",
  limit: 20,
  filter: "title LIKE \"*Swift*\""
) { result in
  switch result {
  case let .success((entities, _)):
    print("The matching entities: \(entities)")
  case let .failure(error):
    print("Get entities request failed with error: \(error.localizedDescription)")
  }
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `next.cursor` from the response and pass it back as `cursor` on the next call. Stop when `next.hasNext` is `false`.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Page through every entity, passing the cursor of each page into the next request
func fetchEntities(cursor: String? = nil) {
  pubnub.dataSync.getEntities(
    className: "course",
    cursor: cursor,
    limit: 20
  ) { result in
    switch result {
    case let .success((entities, next)):
      print("A page of entities: \(entities)")

      // Stop when the response has no next page
      if let next = next, next.hasNext {
        fetchEntities(cursor: next.cursor)
      }
    case let .failure(error):
      print("Get entities request failed with error: \(error.localizedDescription)")
    }
  }
}

fetchEntities()
```

### Set entity

Replaces an entity in full (PUT). Send the complete set of fields, any field you omit is cleared. `className` and `classLevel` are immutable after creation and cannot be sent. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`. For a partial update, use [Update entity](#update-entity).

#### Method(s)

```swift
func setEntity(
    id: String,
    classVersion: Int,
    status: String? = nil,
    payload: JSONCodable? = nil,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncEntity, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Entity identifier. |
| classVersion | Int | Yes |  | Version of the entity class schema. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncEntity`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on a `course` entity, as a type the SDK can encode into a request and decode from a response
struct CourseDetails: JSONCodable {
  let title: String
  let lessonCount: Int
  let isPublished: Bool
  let summary: String?
}

// Replace every mutable field. Every field to keep must be sent back; an omitted field is cleared
pubnub.dataSync.setEntity(
  id: "course-swift-basics",
  classVersion: 1,
  status: "published",
  payload: CourseDetails(
    title: "Swift Basics",
    lessonCount: 30,
    isPublished: true,
    summary: "An introduction to Swift for new developers"
  ),
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(entity):
    print("The set entity: \(entity)")
  case let .failure(error):
    print("Set entity request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "course-swift-basics",
        "entityClass": "course",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "status": "published",
        "payload": {"title": "Swift Basics", "lessonCount": 30, "isPublished": true, "summary": "An introduction to Swift for new developers"},
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T13:00:00.000Z",
        "expiresAt": "2026-08-14T00:00:00.000Z",
        "eTag": "UvWxYzAbCdEfGh"
    }
}
```

### Update entity

Applies a partial update to an entity with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#partial-update) for the operation model.

#### Method(s)

```swift
func updateEntity(
    id: String,
    operations: [PubNubDataSyncPatchOperation],
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncEntity, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Entity identifier. |
| operations | [PubNubDataSyncPatchOperation] | Yes |  | One or more JSON Patch operations. Must contain at least one item. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncEntity`. Pass `nil` to ignore the result. |

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

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Change selected fields, leaving every unaddressed field untouched
pubnub.dataSync.updateEntity(
  id: "course-swift-basics",
  operations: [
    .replace(path: "/payload/title", value: "Swift Basics (2026)"),
    .replace(path: "/payload/lessonCount", value: 32),
    .replace(path: "/payload/isPublished", value: true),
    .remove(path: "/payload/summary")
  ],
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(entity):
    print("The updated entity: \(entity)")
  case let .failure(error):
    print("Update entity request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "course-swift-basics",
        "entityClass": "course",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "status": "published",
        "payload": {"title": "Swift Basics (2026)", "lessonCount": 32, "isPublished": true},
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T13:05:00.000Z",
        "expiresAt": "2026-08-14T00:00:00.000Z",
        "eTag": "WxYzAbCdEfGhIj"
    }
}
```

#### 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 `operations` array. 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.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Combine several operations in one atomic patch, and guard the write with the eTag
pubnub.dataSync.updateEntity(
  id: "course-swift-basics",
  operations: [
    .test(path: "/status", value: "draft"),
    .replace(path: "/payload/lessonCount", value: 30),
    .add(path: "/payload/level", value: "beginner"),
    .copy(from: "/payload/title", path: "/payload/slug"),
    .move(from: "/payload/summary", path: "/payload/description"),
    .remove(path: "/payload/isPublished")
  ],
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(entity):
    print("The patched entity: \(entity)")
  case let .failure(error):
    print("Update entity request failed with error: \(error.localizedDescription)")
  }
}
```

### Remove entity

Deletes an entity by `id`.

#### Method(s)

```swift
func removeEntity(
    id: String,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<Void, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Entity identifier. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds `Void`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Remove an entity
pubnub.dataSync.removeEntity(
  id: "course-swift-basics",
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case .success:
    print("The entity was removed")
  case let .failure(error):
    print("Remove entity request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200
}
```

## Relationships

A relationship links two entities and carries its own `payload`. In the running example, the `enrollment` relationship links `student-alice` to `course-swift-basics`.

Relationships are instances of the [relationship classes](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md) 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](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md) to declare a class, and [Get relationship class by name and version](https://www.pubnub.com/docs/admin-api/get-relationship-class-by-name-and-version.md) 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)

```swift
func createRelationship(
    className: String,
    classVersion: Int,
    entityAId: String,
    entityBId: String,
    id: String? = nil,
    status: String? = nil,
    payload: JSONCodable? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncRelationship, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Optional | `server-generated` | Relationship identifier. Omit to let the server generate a UUID. Max 255 characters. |
| className | String | Yes |  | Name of the relationship class this instance belongs to. Set at creation and immutable afterward. |
| entityAId | String | Yes |  | Identifier of the first linked entity. Immutable after creation. |
| entityBId | String | Yes |  | Identifier of the second linked entity. Immutable after creation. |
| classVersion | Int | Yes |  | Version of the relationship class schema. Sent to the service as `relationshipClassVersion`. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncRelationship`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on an `enrollment` relationship, as a type the SDK can encode into a request
// and decode from a response
struct EnrollmentDetails: JSONCodable {
  let role: String
  let completedLessons: Int
  let isActive: Bool
  let grade: String?
}

// Connect two entities with a relationship of your own class
pubnub.dataSync.createRelationship(
  className: "enrollment",
  classVersion: 1,
  entityAId: "student-alice",
  entityBId: "course-swift-basics",
  id: "alice-course-swift-basics",
  status: "active",
  payload: EnrollmentDetails(
    role: "student",
    completedLessons: 0,
    isActive: true,
    grade: nil
  )
) { result in
  switch result {
  case let .success(relationship):
    print("The created relationship: \(relationship)")
  case let .failure(error):
    print("Create relationship request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice-course-swift-basics",
        "relationshipClass": "enrollment",
        "relationshipClassVersion": 1,
        "entityAId": "student-alice",
        "entityBId": "course-swift-basics",
        "status": "active",
        "payload": {"role": "student", "completedLessons": 0, "isActive": true},
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T09:20:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "YzAbCdEfGhIjKl"
    }
}
```

### Get relationship

Returns a single relationship by `id`.

#### Method(s)

```swift
func getRelationship(
    id: String,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncRelationship, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Relationship identifier. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncRelationship`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on an `enrollment` relationship, as a type the SDK can encode into a request
// and decode from a response
struct EnrollmentDetails: JSONCodable {
  let role: String
  let completedLessons: Int
  let isActive: Bool
  let grade: String?
}

// Retrieve a single relationship by identifier, and decode its payload into your own type
pubnub.dataSync.getRelationship(id: "alice-course-swift-basics") { result in
  switch result {
  case let .success(relationship):
    print("The relationship for `\(relationship.id)`: \(relationship)")
    print("It connects `\(relationship.entityAId)` and `\(relationship.entityBId)`")
    print("Its eTag, used for optimistic concurrency: \(relationship.eTag)")

    do {
      if let details = try relationship.payload?.decode(EnrollmentDetails.self) {
        print("The \(details.role) completed \(details.completedLessons) lessons")
        print("The enrollment is active: \(details.isActive)")
        print("Their grade: \(details.grade ?? "not graded yet")")
      } else {
        print("The relationship has no stored payload")
      }
    } catch {
      print(
        "Could not decode \(relationship.className) version \(relationship.classVersion) " +
          "for relationship \(relationship.id): \(error)"
      )
    }
  case let .failure(error):
    print("Get relationship request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice-course-swift-basics",
        "relationshipClass": "enrollment",
        "relationshipClassVersion": 1,
        "entityAId": "student-alice",
        "entityBId": "course-swift-basics",
        "status": "active",
        "payload": {"role": "student", "completedLessons": 0, "isActive": true},
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T09:20:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "YzAbCdEfGhIjKl"
    }
}
```

### 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](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#sorting-and-pagination).

#### Method(s)

```swift
func getRelationships(
    className: String,
    classVersion: Int? = nil,
    entityAId: String? = nil,
    entityBId: String? = nil,
    cursor: String? = nil,
    limit: Int? = nil,
    filterFast: String? = nil,
    filter: String? = nil,
    sort: [PubNub.DataSyncSortField] = [],
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<(relationships: [PubNubDataSyncRelationship], next: PubNubDataSyncPage?), Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| className | String | Yes |  | Name of the relationship class to list. |
| classVersion | Int | Optional | `all versions` | Relationship class version to list. Omit to list relationships across every version of the class. |
| entityAId | String | Optional |  | List only relationships whose first entity is this id. |
| entityBId | String | Optional |  | List only relationships whose second entity is this id. |
| cursor | String | Optional |  | Opaque pagination cursor. Omit for the first page. |
| limit | Int | Optional | `20` | Maximum number of relationships per page. Max `100`. |
| filterFast | String | Optional |  | 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`. |
| filter | String | Optional |  | 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`. |
| sort | [PubNub.DataSyncSortField] | Optional | `[]` | Order results. Each `PubNub.DataSyncSortField` names a `property` and sets `ascending` (default `true`). |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a tuple of the `relationships` array and the optional `next` page. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve a page of relationships of your own class
pubnub.dataSync.getRelationships(className: "enrollment", limit: 20) { result in
  switch result {
  case let .success((relationships, next)):
    print("The relationships: \(relationships)")
    print("The next page used for pagination: \(String(describing: next))")
  case let .failure(error):
    print("Get relationships request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "alice-course-swift-basics",
            "relationshipClass": "enrollment",
            "relationshipClassVersion": 1,
            "entityAId": "student-alice",
            "entityBId": "course-swift-basics",
            "status": "active",
            "payload": {"role": "student", "completedLessons": 0, "isActive": true},
            "createdAt": "2026-07-13T09:20:00.000Z",
            "updatedAt": "2026-07-13T09:20:00.000Z",
            "expiresAt": "2026-08-13T00:00:00.000Z",
            "eTag": "YzAbCdEfGhIjKl"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### List an entity's outgoing links with entityAId

Pass `entityAId` to list relationships where the entity is on the first side of the link.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve the relationships whose side A is a specific entity
pubnub.dataSync.getRelationships(
  className: "enrollment",
  entityAId: "student-alice",
  limit: 20
) { result in
  switch result {
  case let .success((relationships, _)):
    print("The courses `student-alice` enrolled in: \(relationships.map { $0.entityBId })")
  case let .failure(error):
    print("Get relationships request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Retrieve the relationships whose side B is a specific entity
pubnub.dataSync.getRelationships(
  className: "enrollment",
  entityBId: "course-swift-basics",
  limit: 20
) { result in
  switch result {
  case let .success((relationships, _)):
    print("The students enrolled in `course-swift-basics`: \(relationships.map { $0.entityAId })")
  case let .failure(error):
    print("Get relationships request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter strongly consistent data, so a relationship you just wrote is matched
pubnub.dataSync.getRelationships(
  className: "enrollment",
  limit: 20,
  filterFast: "isActive == true && completedLessons >= 10"
) { result in
  switch result {
  case let .success((relationships, _)):
    print("The matching relationships: \(relationships)")
  case let .failure(error):
    print("Get relationships request failed with error: \(error.localizedDescription)")
  }
}
```

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

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Filter eventually consistent data with the full expression language
pubnub.dataSync.getRelationships(
  className: "enrollment",
  limit: 20,
  filter: "role LIKE \"*student*\""
) { result in
  switch result {
  case let .success((relationships, _)):
    print("The matching relationships: \(relationships)")
  case let .failure(error):
    print("Get relationships request failed with error: \(error.localizedDescription)")
  }
}
```

##### Page through results with cursor

Pass no `cursor` on the first call. Take `next.cursor` from the response and pass it back as `cursor` on the next call. Stop when `next.hasNext` is `false`.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Page through every relationship, passing the cursor of each page into the next request
func fetchRelationships(cursor: String? = nil) {
  pubnub.dataSync.getRelationships(
    className: "enrollment",
    cursor: cursor,
    limit: 20
  ) { result in
    switch result {
    case let .success((relationships, next)):
      print("A page of relationships: \(relationships)")

      // Stop when the response has no next page
      if let next = next, next.hasNext {
        fetchRelationships(cursor: next.cursor)
      }
    case let .failure(error):
      print("Get relationships request failed with error: \(error.localizedDescription)")
    }
  }
}

fetchRelationships()
```

### 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 `setRelationship` doesn't take `entityAId` or `entityBId`, there is nothing to resend. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#optimistic-concurrency-with-etags) for `ifMatchesEtag`. For a partial update, use [Update relationship](#update-relationship).

#### Method(s)

```swift
func setRelationship(
    id: String,
    classVersion: Int,
    status: String? = nil,
    payload: JSONCodable? = nil,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncRelationship, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Relationship identifier. |
| classVersion | Int | Yes |  | Version of the relationship class schema. Sent to the service as `relationshipClassVersion`. |
| status | String | Optional |  | Free-form lifecycle status. Max 100 characters. |
| payload | JSONCodable | Optional |  | Free-form JSON object holding your application data. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncRelationship`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// The fields stored on an `enrollment` relationship, as a type the SDK can encode into a request
// and decode from a response
struct EnrollmentDetails: JSONCodable {
  let role: String
  let completedLessons: Int
  let isActive: Bool
  let grade: String?
}

// Replace every mutable field. Every field to keep must be sent back; an omitted field is cleared
pubnub.dataSync.setRelationship(
  id: "alice-course-swift-basics",
  classVersion: 1,
  status: "active",
  payload: EnrollmentDetails(
    role: "student",
    completedLessons: 12,
    isActive: true,
    grade: "A"
  ),
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(relationship):
    print("The set relationship: \(relationship)")
  case let .failure(error):
    print("Set relationship request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice-course-swift-basics",
        "relationshipClass": "enrollment",
        "relationshipClassVersion": 1,
        "entityAId": "student-alice",
        "entityBId": "course-swift-basics",
        "status": "active",
        "payload": {"role": "student", "completedLessons": 12, "isActive": true, "grade": "A"},
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T14:00:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "AbCdEfGhIjKlMn"
    }
}
```

### Update relationship

Applies a partial update to a relationship with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/data-storage/structured-data/data-operations.md#partial-update) for the operation model.

#### Method(s)

```swift
func updateRelationship(
    id: String,
    operations: [PubNubDataSyncPatchOperation],
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<PubNubDataSyncRelationship, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Relationship identifier. |
| operations | [PubNubDataSyncPatchOperation] | Yes |  | One or more JSON Patch operations. Must contain at least one item. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds a `PubNubDataSyncRelationship`. Pass `nil` to ignore the result. |

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

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Change selected fields, leaving every unaddressed field untouched
pubnub.dataSync.updateRelationship(
  id: "alice-course-swift-basics",
  operations: [
    .replace(path: "/payload/completedLessons", value: 24),
    .replace(path: "/payload/isActive", value: false),
    .remove(path: "/payload/grade")
  ],
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case let .success(relationship):
    print("The updated relationship: \(relationship)")
  case let .failure(error):
    print("Update relationship request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "alice-course-swift-basics",
        "relationshipClass": "enrollment",
        "relationshipClassVersion": 1,
        "entityAId": "student-alice",
        "entityBId": "course-swift-basics",
        "status": "active",
        "payload": {"role": "student", "completedLessons": 24, "isActive": false},
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T14:05:00.000Z",
        "expiresAt": "2026-08-13T00:00:00.000Z",
        "eTag": "CdEfGhIjKlMnOp"
    }
}
```

### Remove relationship

Deletes a relationship by `id`.

#### Method(s)

```swift
func removeRelationship(
    id: String,
    ifMatchesEtag: String? = nil,
    custom requestConfig: PubNub.RequestConfiguration = PubNub.RequestConfiguration(),
    completion: ((Result<Void, Error>) -> Void)?
)
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id | String | Yes |  | Relationship identifier. |
| ifMatchesEtag | String | Optional |  | The `eTag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |
| custom | PubNub.RequestConfiguration | Optional | `PubNub.RequestConfiguration()` | Per-request overrides for the PubNub configuration or network session. Refer to [request configuration](https://www.pubnub.com/docs/sdks/swift/api-reference/configuration.md#request-configuration) for details. |
| completion | Closure | Yes |  | Receives the `Result` of the call. On success it holds `Void`. Pass `nil` to ignore the result. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Remove a relationship
pubnub.dataSync.removeRelationship(
  id: "alice-course-swift-basics",
  ifMatchesEtag: "3w5e111uk7djz" // Always provide the eTag from the most recent response
) { result in
  switch result {
  case .success:
    print("The relationship was removed")
  case let .failure(error):
    print("Remove relationship request failed with error: \(error.localizedDescription)")
  }
}
```

#### Response

```json
{
    "status": 200
}
```

## Real-time updates

DataSync objects can publish `create`, `update`, and `delete` events that you receive in real time with the `onDataSync` listener. Create a subscription for the object's channel, assign `onDataSync` on the `Subscription`, a `SubscriptionSet`, or the `PubNub` client, and call `subscribe()`, as described in [Add DataSync listener](https://www.pubnub.com/docs/sdks/swift/api-reference/publish-and-subscribe.md#add-datasync-listener).

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Create a subscription for a custom projection of a Data Sync entity
let dataSyncSubscription = pubnub
  .dataSyncEntity("match-1")
  .subscription(projection: "stats")

// Add a listener to receive Data Sync entity and relationship events
dataSyncSubscription.onDataSync = { dataSyncEvent in
  print("Received on \(dataSyncEvent.channel) at \(dataSyncEvent.timetoken)")

  // Create and update changes can be read two ways: switch on `object` to handle each type
  // separately, or read `entity` / `relationship` to handle every type the same way
  switch dataSyncEvent.change {
  case let .entityCreated(entityEvent):
    switch entityEvent.object {
    case let .user(user):
      print("User \(user.id) was created.")
    case let .channel(channel):
      print("Channel \(channel.id) was created.")
    case let .custom(entity):
      print("Entity \(entity.id) of class \(entity.className) was created.")
    }
  case let .entityUpdated(entityEvent):
    let entity = entityEvent.entity
    print("Entity \(entity.id) was updated.")
    print("Entity kind: \(entityEvent.kind)")
  case let .entityDeleted(entityEvent):
    let removedEntity = entityEvent.removed
    print("Entity \(removedEntity.id) was deleted.")
    print("Entity kind: \(entityEvent.kind)")
  case let .relationshipCreated(relationshipEvent):
    switch relationshipEvent.object {
    case let .membership(membership):
      print("User \(membership.userId) joined channel \(membership.channelId).")
    case let .custom(relationship):
      print("Relationship \(relationship.id) of class \(relationship.className) was created.")
    }
  case let .relationshipUpdated(relationshipEvent):
    let relationship = relationshipEvent.relationship
    print("Relationship \(relationship.id) was updated.")
    print("Relationship kind: \(relationshipEvent.kind)")
  case let .relationshipDeleted(relationshipEvent):
    let removedRelationship = relationshipEvent.removed
    print("Relationship \(removedRelationship.id) was deleted.")
    print("Relationship kind: \(relationshipEvent.kind)")
  case let .unknown(unknownEvent):
    // An object type or action this SDK version doesn't recognize
    print("Unrecognized \(unknownEvent.type) change: \(unknownEvent.event)")
  }
}
```

Each event names the change in `change` (`entityCreated`, `entityUpdated`, `entityDeleted`, `relationshipCreated`, `relationshipUpdated`, or `relationshipDeleted`), identifies the object kind in `kind`, and carries the object state in `entity` or `relationship`. For a `delete` event, `removed` carries the `id`, the class details, and the `deletedAt` timestamp. A change the SDK doesn't recognize arrives as `unknown`, with the raw `metadata` and `payload` attached.

`kind` names the object kind, while the state always arrives in one of the two containers:

| Change to | `kind` | State arrives in |
| --- | --- | --- |
| A user | `user` | `PubNubDataSyncEntityEvent` (`entity`) |
| A channel | `channel` | `PubNubDataSyncEntityEvent` (`entity`) |
| An entity | `custom` | `PubNubDataSyncEntityEvent` (`entity`) |
| A membership | `membership` | `PubNubDataSyncRelationshipEvent` (`relationship`) |
| A relationship | `custom` | `PubNubDataSyncRelationshipEvent` (`relationship`) |

Dispatch on `kind` rather than on which container is populated, because `PubNubDataSyncEntityEvent` is shared by three kinds and `PubNubDataSyncRelationshipEvent` by two. Read `object` on either container to get the state narrowed to `PubNubDataSyncUser`, `PubNubDataSyncChannel`, `PubNubDataSyncMembership`, or the general entity and relationship types. `className` tells you the specific class within a kind, for example which subclass of `User` an event belongs to.

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

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

For a membership event, `relationship.entityAId` is the channel id and `relationship.entityBId` is the user id. The service sends these as `channelId` and `userId`, and the SDK maps them onto the A and B sides so that one `PubNubDataSyncRelationship` type serves both memberships and your own relationships. The narrowed `PubNubDataSyncMembership` surfaces them as `channelId` and `userId` again.

### Projection channels

Each [projection](https://www.pubnub.com/docs/data-storage/structured-data/projections.md) 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 `course-swift-basics` | The fields tagged `__default__` |
| Any named projection, for example `admin` | `__<projection>__<id>`, for example `__admin__course-swift-basics` | The fields tagged with that projection |

A projection name must start with a letter or digit, so a leading `__` marks a projection channel, with `__default__` as the only exception.

:::warning Avoid ids that start with two underscores
A projection channel is named `__<projection>__<id>`, so an object id in that form can collide with another object's projection channel. The service doesn't reject such ids today, so avoid any id that starts with `__`.
:::

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 a `stats` projection publishes two events, and a client subscribed only to `course-swift-basics` never sees the `stats`-only fields. The SDK builds the channel name for you: call `subscription()` for the base projection, or `subscription(projection:)` with the projection name for a named one.

```swift
import PubNubSDK

// Initializes a PubNub object with the configuration
let pubnub = PubNub(
  configuration: PubNubConfiguration(
    publishKey: "demo",
    subscribeKey: "demo",
    userId: "myUniqueUserId"
  )
)

// Subscribe to the base projection and to a named projection of the same entity
let baseProjectionSubscription = pubnub
  .dataSyncEntity("course-swift-basics")
  .subscription()

let statsProjectionSubscription = pubnub
  .dataSyncEntity("course-swift-basics")
  .subscription(projection: "stats")

baseProjectionSubscription.subscribe()
statsProjectionSubscription.subscribe()
```

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.

:::warning Projection channels need channel grants
Publishing to `__<projection>__<id>` is an ordinary channel publish, and it isn't gated by the `dataSyncProjections` entries in a token. Use Access Manager `channels` grants to control who can subscribe to a projection channel. Anyone who can subscribe there receives the restricted fields. Refer to [grant structured data access](https://www.pubnub.com/docs/data-storage/structured-data/grant-structured-data-access.md).
:::

Delete events are never filtered by a projection.

Events are off by default and are enabled per class in the [Admin Portal](https://admin.pubnub.com). Refer to [enabling events](https://www.pubnub.com/docs/data-storage/structured-data/events.md#enable-datasync-events) for how to turn them on, and [receiving events](https://www.pubnub.com/docs/data-storage/structured-data/events.md) for the listener flow.

Last updated at: 2026-10-13T07:06:43.000Z
