> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift V4→V5 API Migration Guide

> Guide for migrating Swift apps from Ditto v4 to v5.

## Overview

This guide covers the essential changes needed to migrate your Ditto Swift app from v4 to v5. The main architectural shift is moving from identity-based initialization to a four-phase configuration model with async/await APIs.

**Also required:** v5 uses DQL (Ditto Query Language) for all data operations. See the [DQL Migration Guide](./swift-dql) for query migration steps.

***

## AI Agent Prompt

Use this prompt when working with an AI coding assistant to migrate your Ditto Swift app from v4 to v5.

<Accordion title="Copy AI Migration Prompt (Click to Expand)">
  ````text theme={null}
  I need help migrating a Ditto Swift application from v4 to v5. Focus on these critical changes:

  INITIALIZATION:


  - Parameter labels are case-sensitive: `databaseID` (uppercase `ID`), and `ditto.config.databaseID` replaces `ditto.appID`.
  - `DittoSwift` no longer re-exports `Foundation`. In v4, importing `DittoSwift` transitively brought in `Foundation` symbols (`URL`, `Date`, `NSLog`, `Notification`, etc.); in v5 it does not. Any file that uses a Foundation type now needs an explicit `import Foundation`. Audit every file that imports `DittoSwift` without also importing `Foundation`.
  - In v4 when adding the `DittoSwift` package, it would add in the `DittoObjC` package product dependencies.  In v5, `DittoObjC` is no longer a separate product — it's bundled into `DittoSwift`.  This means that you no longer need to add the `DittoObjC` package product dependencies to your project and you need to remove the `DittoObjC` package product dependencies from your project in order to get the project to build.
  - These are the following places the `DittoObjC` package product dependencies are usually used:
      - 1. `PBXBuildFile` section — removed the `DittoObjC in Frameworks` entry
      - 2. `PBXFrameworksBuildPhase` — removed `DittoObjC in Frameworks` from the `files` array
      - 3. `PBXNativeTarget.packageProductDependencies` — removed the `DittoObjC` reference
      - 4. `XCSwiftPackageProductDependency` — removed the entire `DittoObjC` dependency entry
  - Replace `Ditto(identity:)` with `try await Ditto.open(config:)` using async/await.
  - Create a `DittoConfig` with `databaseID` and a connect mode
  - `.onlinePlayground(appID:token:enableDittoCloudSync:customAuthURL:)` → `DittoConfig(databaseID: appID, connect: .server(url:))`
  - `.onlineWithAuthentication(appID:enableDittoCloudSync:customAuthURL:)` → `DittoConfig(databaseID: appID, connect: .server(url:))` + auth `expirationHandler`
  - `.offlinePlayground(appID:)` → `DittoConfig(databaseID: appID, connect: .smallPeersOnly())`
  - `.offlinePlayground()` (zero-arg form, no `appID`) → `DittoConfig(databaseID: "<your-database-id>", connect: .smallPeersOnly())`. The zero-arg v4 form had no `appID`, but v5 `databaseID` is required — supply any non-empty string. For tests/playgrounds, `"test"` is fine.
  - `.sharedKey(appID:sharedKey:)` → `DittoConfig(databaseID: appID, connect: .smallPeersOnly(privateKey: sharedKey))`
  - `.manual(certificateConfig:)` → **No replacement** — removed in v5
  - `.smallPeersOnly()` is equivalent to `.smallPeersOnly(privateKey: nil)` — the parameter has a default value of `nil`, so prefer the shorter form when there is no shared key.
  - The `Ditto(identity:)` initializer is removed. You MUST use `try await Ditto.open(config:)` or `Ditto.openSync(config:)`
  - Remove `disableSyncWithV3()` calls — v3 protocol is fully dropped in v5
  - Remove `updateTransportConfig` workaround patterns for adding websocket URLs — `DittoConfig` handles the server URL directly. Note: `updateTransportConfig` itself still exists in v5 for advanced transport configuration
  - Remove `ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")` — v5 defaults to `false`
  - IMPORTANT: If your v4 app used the default (strict=true), set `DQL_STRICT_MODE=true` BEFORE starting sync to maintain v4 behavior. Objects that were replaced whole in v4 will silently merge at field level in v5 without this.
  - v5 default persistence directory is `ditto-{databaseID}` (was `ditto` in v4). Set `persistenceDirectory` explicitly if maintaining v4 directory structure.
  - `persistenceDirectory` is an initializer parameter on `DittoConfig`. The full init signature is:
    ```swift
    public init(databaseID: String,
                connect: DittoConfigConnect,
                persistenceDirectory: URL? = nil,
                experimental: DittoConfigExperimental = .init())
    ```
    Pass it at construction:
    ```swift
    let config = DittoConfig(
        databaseID: "my-app",
        connect: .server(url: serverURL),
        persistenceDirectory: URL(fileURLWithPath: "/app/sandbox/ditto")
    )
    ```
  - Configure logging via the `DittoLogger` type (e.g. `DittoLogger.minimumLogLevel = .info`); `DittoConfig` itself exposes only `databaseID`, `connect`, `persistenceDirectory`, and `experimental`.
  - Create the instance with `try await Ditto.open(config:)` (async) or `Ditto.openSync(config:)` (sync).

  SYNC:
  The sync API moved from top-level Ditto methods to the `ditto.sync` sub-object.
  - `try ditto.startSync()` → `try ditto.sync.start()`
  - `ditto.stopSync()` → `ditto.sync.stop()`
  - `ditto.isSyncActive` → `ditto.sync.isActive`

  AUTHENTICATION:
  The `DittoAuthenticationDelegate` protocol is removed. Replace with the `expirationHandler` closure.
  - Set `ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in ... }` after `Ditto.open(config:)`
  - The handler signature is `@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void`
  - Inside the handler, call `expiredDitto.auth?.login(token:provider:completion:)` (use a distinct closure parameter name to avoid shadowing the outer `ditto`)
  - Use `.development` provider for playground/test tokens
  - The handler is called when `timeUntilExpiration == 0` (initial auth or expired) and when `timeUntilExpiration > 0` (token expiring soon — refresh). Handle both cases.
  - `authenticator.loginWithToken(_:provider:completion:)` → `ditto.auth?.login(token:provider:completion:)`
  - `DittoAuthenticationDelegate.authenticationRequired(authenticator:)` → removed, use `expirationHandler` with `timeUntilExpiration == 0`
  - `DittoAuthenticationDelegate.authenticationExpiringSoon(authenticator:secondsRemaining:)` → removed, use `expirationHandler` with `timeUntilExpiration > 0`
  - `ditto.delegate = MyAuthDelegate()` → `ditto.auth?.expirationHandler = { ... }`

  OBSERVERS:
  The callback-based `registerObserver` API is the same in v5 — it returns `DittoStoreObserver` with callback handlers.
  - `ditto.store.registerObserver(query:arguments:deliverOn:handler:)` → same API, returns `DittoStoreObserver`
  - v5 adds `Codable` argument overloads: `registerObserver(query:arguments: Codable, deliverOn:handler:)`
  - Call `item.dematerialize()` after extracting data from `DittoQueryResultItem` to free native memory — do not use the item after this call
  - `observer.stop()` / `observer.cancel()` for cleanup is still the pattern

  BACK PRESSURE (Signal Next):
  Use `registerObserver(query:arguments:deliverOn:handlerWithSignalNext:)` when your handler needs time to process results.
  - Changes that occur while your handler is processing are coalesced — you get one callback with the latest state, not every intermediate change
  - You must call `signalNext()` to receive the next update
  - Standard observer auto-signals after handler returns

  COLLECTION API → DQL:
  All `.collection()` APIs are **removed**. Replace with `store.execute()` using DQL.
  - `ditto.store.collection("cars").find("color == 'blue'").exec()` → `try await ditto.store.execute(query: "SELECT * FROM cars WHERE color = :color", arguments: ["color": "blue"])`
  - `ditto.store.collection("cars").findByID(id).exec()` → `try await ditto.store.execute(query: "SELECT * FROM cars WHERE _id = :id", arguments: ["id": id])`
  - `ditto.store.collection("cars").upsert(value)` → `try await ditto.store.execute(query: "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE", arguments: ["doc": value])`
  - `ditto.store.collection("cars").findByID(id).update { doc in doc?["color"].set("green") }` → `try await ditto.store.execute(query: "UPDATE cars SET color = :color WHERE _id = :id", arguments: ["color": "green", "id": id])`
  - `ditto.store.collection("cars").findByID(id).remove()` → `try await ditto.store.execute(query: "DELETE FROM cars WHERE _id = :id", arguments: ["id": id])`
  - `ditto.store.collection("cars").findByID(id).evict()` → `try await ditto.store.execute(query: "EVICT FROM cars WHERE _id = :id", arguments: ["id": id])`
  - Always use parameterized queries with `:paramName` — NEVER use string interpolation in DQL queries

  LIVE QUERIES → STORE OBSERVERS:
  - `collection.find("...").observeLocal { docs, event in ... }` → `ditto.store.registerObserver(query: "SELECT * FROM ...", arguments: [...]) { result in ... }`
  - `liveQuery?.stop()` → `observer.stop()`
  - v4 handler: `(docs: [DittoDocument], event: DittoLiveQueryEvent) -> Void`
  - v5 handler: `(result: DittoQueryResult) -> Void` where result has `.items` containing `DittoQueryResultItem` objects
  - Extract your model from `item.jsonData()` then call `item.dematerialize()` to free native memory
  - When using `item.jsonData()`, you need to make sure you import the Swift Foundation framework

  SUBSCRIPTIONS:
  - `ditto.store.collection("cars").find("color == 'red'").subscribe()` → `try ditto.sync.registerSubscription(query: "SELECT * FROM cars WHERE color = :color", arguments: ["color": "red"])`
  - `DittoSubscription` → replaced by `DittoSyncSubscription`

  COUNTERS:
  - `doc?["viewCount"].counter?.increment(by: 1.0)` → `try await ditto.store.execute(query: "UPDATE cars APPLY viewCount PN_INCREMENT BY :n WHERE _id = :id", arguments: ["n": 1, "id": id])`
  - DO NOT initialize counter fields in insert documents. Counters are created on first `PN_INCREMENT`. Inserting `0` creates a REGISTER, not a COUNTER.
  - Decrement by passing a negative value: `["n": -1]`

  PRESENCE:
  - `peer.peerKeyString` → `peer.peerKey` (type is `String` in both v4 and v5, but property renamed)
  - `peer.osV2` → `peer.os`
  - `peer.isConnectedToDittoCloud` → `peer.isConnectedToDittoServer`
  - `connection.peerKeyString1` / `peerKeyString2` → `connection.peer1` / `connection.peer2` (now `String`)
  - `connection.approximateDistanceInMeters` → **removed, no replacement**
  - `ditto.observePeers(_:)` → `ditto.presence.observe(didChangeHandler:)`
  - `DittoRemotePeer` struct (deprecated) → use `DittoPeer` in `DittoPresenceGraph`
  - `DittoAddress` struct → removed

  DITTO CLASS:
  - `Ditto(identity:)` → `try await Ditto.open(config:)` or `Ditto.openSync(config:)`
  - `ditto.activated` → `ditto.isActivated`
  - `ditto.appID` → `ditto.config.databaseID`
  - `ditto.transportDiagnostics()` → removed
  - `ditto.runGarbageCollection()` → removed (automatic in v5)
  - `ditto.disableSyncWithV3()` → removed (v3 protocol dropped)
  - `ditto.isHistoryTrackingEnabled` → removed

  DISK USAGE:
  - `DiskUsage` → `DittoDiskUsage`
  - `diskUsage.exec` → `diskUsage.item`
  - `DiskUsageObserverHandle` → `DittoDiskUsageObserver`
  - `DiskUsageItem` → `DittoDiskUsageItem` (property names `.path`, `.sizeInBytes`, `.childItems` are unchanged)

  LOGGING:
  - `DittoLogger` class name is unchanged in both v4 and v5, but is now `final` and `Sendable` in v5
  - `DittoLogger.setLogFile()` → removed — use `export(to:)` instead

  ERRORS:
  - `DittoSwiftError` typealias → removed, use `DittoError` directly
  - `migrationError` error case → removed

  TRANSPORT CONFIG:
  - `DittoHTTPListenConfig.staticContentPath` → removed
  - Default `webSocketSync` — check if your app relied on v4 default; explicitly set `true` if needed

  TYPES & PROTOCOLS:
  - `DittoConnectionPriority` enum → removed
  - `DittoTransportSnapshot` struct → removed (with transport diagnostics)
  - `DittoExperimental.jsonByTranscoding(cbor:)` → removed
  - All public types are now `final` and `Sendable` for Swift concurrency
  - `DittoConfig` and `DittoConfigConnect` conform to `Sendable` and `Codable`
  - `DittoConnection` now conforms to `Identifiable`, `Equatable`, `Hashable`, and `Sendable`

  SWIFT 6 COMPATIBILITY:
  - `DittoConnection` gaining `Identifiable` conformance causes `filter` ambiguity in Swift 6. Fix: annotate closure parameter explicitly:
    ```swift
    // BROKEN in Swift 6
    connections.filter { $0.peer1 == localKey }
    // FIXED
    connections.filter { (conn: DittoConnection) in conn.peer1 == localKey }
    ```
  - Auth handler: `expirationHandler` closure is `@Sendable` and `async`. If your Ditto manager is an `actor`, use `await` when accessing actor-isolated state inside `Task { }` blocks within the handler.

  REMOVED APIs (no replacement):
  - `ditto.transportDiagnostics()` — low-level transport debug info
  - `ditto.runGarbageCollection()` — CRDT GC controls
  - `ditto.disableSyncWithV3()` — v3 protocol fully dropped
  - `ditto.isHistoryTrackingEnabled` — history tracking removed
  - `DittoIdentity.manual(certificateConfig:)` — manual identity type
  - `Connection.approximateDistanceInMeters` — peer distance estimation
  - `DittoHTTPListenConfig.staticContentPath` — static file serving
  - `DittoStore.queriesHash(queries:)` — hash-based query deduplication
  - `DittoStore.queriesHashMnemonic(queries:)` — query mnemonic hashing
  - `DittoConnectionPriority` enum — connection priority
  - `DittoTransportSnapshot` struct — removed with transport diagnostics
  - `DittoRemotePeer` struct (deprecated) — use `DittoPeer` in `DittoPresenceGraph`
  - `DittoAddress` struct — removed
  - `DittoExperimental.jsonByTranscoding(cbor:)` — CBOR-to-JSON utility
  - `DittoSwiftError` typealias — use `DittoError` directly
  - `migrationError` error case — removed
  - `DittoLogger.setLogFile()` — use `export(to:)` instead

  NEW APIs:
  - `Ditto.open(config:): async throws -> Ditto` — async factory method
  - `Ditto.openSync(config:): throws -> Ditto` — synchronous factory alternative
  - `ditto.config: DittoConfig` — read the config used to open this instance
  - `ditto.isActivated: Bool` — replaces `ditto.activated`
  - `DittoStore.newAttachment(data: Data, metadata:)` — create attachment from in-memory `Data` (v4 was file path only)
  - `DittoStore.registerObserver(query:arguments: some Codable, ...)` — overloads accepting `Codable` query arguments
  - `DittoSync.registerSubscription(query:arguments: some Codable)` — overload accepting `Codable` arguments
  - `DittoDiskUsageObserver` — standalone observer class (was nested `DiskUsage.DiskUsageObserverHandle`)
  - `DittoDiskUsageItem` — renamed from `DiskUsageItem`
  - `DittoSyncSubscription.queryArgumentsCBORData` / `.queryArgumentsJSONData` / `.id` — introspection properties
  - `DittoConnection` conforms to `Identifiable`, `Equatable`, `Hashable`, and `Sendable`
  - All public types now `final` and `Sendable` for Swift concurrency
  - `DittoConfig` and `DittoConfigConnect` conform to `Sendable` and `Codable`
  - `DittoSmallPeerInfo.metadataJSONString` removed — use `metadataJSONData` instead (which already existed in v4)

  BEFORE (v4):
  ```swift
  let ditto = Ditto(
      identity: .onlinePlayground(
          appID: "your-app-id",
          token: "your-token",
          enableDittoCloudSync: false,
          customAuthURL: URL(string: "https://your-server.ditto.live")
      )
  )

  ditto.updateTransportConfig { config in
      config.connect.webSocketURLs.insert("wss://...")
  }
  ditto.disableSyncWithV3()
  try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")
  try? ditto.startSync()

  // Observer — callback with manual cleanup
  var observer: DittoStoreObserver?
  observer = ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      DispatchQueue.main.async {
          self.cars = result.items.compactMap { Car($0.jsonData()) }
      }
  }

  // Must manually stop in deinit
  deinit { observer?.stop() }

  // Presence
  ditto.observePeers { peers in
      for peer in peers {
          print(peer.peerKeyString, peer.isConnectedToDittoCloud)
      }
  }

  // Cleanup
  observer?.stop()
  ditto.stopSync()
  ```

  AFTER (v5):
  ```swift
  let config = DittoConfig(
      databaseID: "your-database-id",
      connect: .server(url: URL(string: "https://your-server.ditto.live")!)
  )
  let ditto = try await Ditto.open(config: config)

  // Authentication
  ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
      expiredDitto.auth?.login(token: "your-token", provider: .development) { clientInfo, error in
          if let error = error {
              print("Ditto auth failed: \(error), client info: \(String(describing: clientInfo)), time until expiration: \(timeUntilExpiration)s")
          }
      }
  }

  // If maintaining v4 behavior, set strict mode BEFORE starting sync
  // try await ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = true")

  try ditto.sync.start()

  // Observer — callback-based, same pattern as v4 but with dematerialize()
  let observer = try ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      let cars = result.items.compactMap { item -> Car? in
          let car = Car(item.jsonData())
          item.dematerialize()  // Free native memory — do not use item after this
          return car
      }
      DispatchQueue.main.async { self.cars = cars }
  }
  // Cleanup: observer.stop()

  // Subscription — DQL-based
  let sub = try ditto.sync.registerSubscription(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  )

  // Presence
  ditto.presence.observe { graph in
      let allPeers = [graph.localPeer] + graph.remotePeers
      for peer in allPeers {
          print(peer.peerKey, peer.isConnectedToDittoServer)
      }
  }

  // Cleanup
  sub.cancel()
  ditto.sync.stop()
  ```

  CHECKLIST:

  Initialization:
  - Replace `Ditto(identity:)` with `try await Ditto.open(config:)`
  - Add `try await` for initialization — `Ditto.open(config:)` is async and throws
  - Create `DittoConfig` with `databaseID` and connect mode (`.server(url:)` or `.smallPeersOnly(privateKey:)`)
  - Set `DQL_STRICT_MODE=true` BEFORE `sync.start()` if maintaining v4 strict behavior
  - Set `persistenceDirectory` if maintaining v4 directory structure
  - Remove `disableSyncWithV3()` calls
  - Remove `updateTransportConfig` workaround patterns for websocket URLs (method still exists for advanced use)
  - Remove `ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")` (v5 default)

  Authentication:
  - Remove `DittoAuthenticationDelegate` conformance and class
  - Remove `ditto.delegate = MyAuthDelegate()` assignment
  - Set `ditto.auth?.expirationHandler` closure after `Ditto.open(config:)`
  - Handler signature is `@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void`
  - Use `timeUntilExpiration == 0` check for initial auth vs `> 0` for refresh
  - Use `.development` provider for playground/test tokens

  Sync:
  - Replace `try ditto.startSync()` with `try ditto.sync.start()`
  - Replace `ditto.stopSync()` with `ditto.sync.stop()`
  - Replace `ditto.isSyncActive` with `ditto.sync.isActive`

  Observers:
  - Replace `collection.observeLocal { }` with `store.registerObserver(query:arguments:handler:)`
  - Call `item.dematerialize()` after extracting data from `DittoQueryResultItem`
  - Use `handlerWithSignalNext:` for back-pressure control on expensive operations
  - Cleanup with `observer.stop()`

  Data Operations:
  - Replace all `.collection("x").find(...)` chains with `store.execute(query:arguments:)` DQL
  - Use parameterized queries with `:paramName` — never string interpolation
  - Replace `.upsert()` with `INSERT INTO ... DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE`
  - Replace `.update { }` closures with `UPDATE SET` DQL
  - Replace `.remove()` with `DELETE FROM`
  - Replace `.evict()` with `EVICT FROM`
  ````
</Accordion>

***

## DittoObjC Removal

In v4 when adding the `DittoSwift` package, it would add in the `DittoObjC` package product dependencies.  In v5, `DittoObjC` is no longer a separate product — it's bundled into `DittoSwift`.  This means that you no longer need to add the `DittoObjC` package product dependencies to your project and you need to remove the `DittoObjC` package product dependencies from your project in order to get the project to build.

These are the following places the `DittoObjC` package product dependencies are used:

1. `PBXBuildFile` section — removed the `DittoObjC in Frameworks` entry
2. `PBXFrameworksBuildPhase` — removed `DittoObjC in Frameworks` from the `files` array
3. `PBXNativeTarget.packageProductDependencies` — removed the `DittoObjC` reference
4. `XCSwiftPackageProductDependency` — removed the entire `DittoObjC` dependency entry

## Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:

<CodeGroup>
  ```swift SWIFT (v5) theme={null}
  // 1. Configure
  let config = DittoConfig(
      databaseID: "your-database-id",
      connect: .server(url: URL(string: "https://your-server.ditto.live")!)
  )

  // 2. Initialize (async)
  let ditto = try await Ditto.open(config: config)

  // 3. Authenticate
  ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
      expiredDitto.auth?.login(token: "your-token", provider: .development) { clientInfo, error in
          if let error = error {
              print("Ditto auth failed: \(error), client info: \(String(describing: clientInfo)), time until expiration: \(timeUntilExpiration)s")
          }
      }
  }

  // 4. Start sync
  try ditto.sync.start()

  ```

  ```swift SWIFT (v4) theme={null}
  let ditto = Ditto(
      identity: .onlinePlayground(
          appID: "your-app-id",
          token: "your-token",
          enableDittoCloudSync: false,
          customAuthURL: URL(string: "https://your-server.ditto.live")
      )
  )

  ditto.updateTransportConfig { config in
      config.connect.webSocketURLs.insert("wss://...")
  }
  try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")
  ditto.disableSyncWithV3()

  try? ditto.startSync()
  ```
</CodeGroup>

**These changes provide:**

* Clear separation of connection, initialization, and authentication concerns
* Async initialization prevents blocking the main thread
* No workarounds needed (transport config, DQL strict mode, v3 sync disable)
* Type-safe configuration with compile-time validation
* Ditto.open() returns a non-optional Ditto instance

**Initialization Migration Steps**

<Steps>
  <Step title="Replace Configuration and Initialization">
    Replace `Ditto(identity:)` constructor with `Ditto.open(config:)`.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      let config = DittoConfig(
          databaseID: "your-database-id",
          connect: .server(url: URL(string: "https://your-server.ditto.live")!)
      )
      let ditto = try await Ditto.open(config: config)
      ```

      ```swift SWIFT (v4) theme={null}
      let ditto = Ditto(
          identity: .onlinePlayground(
              appID: "your-app-id",
              token: "your-token",
              enableDittoCloudSync: false,
              customAuthURL: URL(string: "https://your-server.ditto.live")
          )
      )
      ```
    </CodeGroup>

    **Key changes:**

    * Use `DittoConfig` instead of `DittoIdentity`
    * `appID` → `databaseID`
    * `.onlinePlayground` → `.server(url:)`
    * `.offlinePlayground` → `.smallPeersOnly(privateKey: nil)`
    * Add `try await` for async initialization
    * Remove `updateTransportConfig`, `disableSyncWithV3()`, and DQL strict mode queries
  </Step>

  <Step title="Update Authentication">
    Set authentication handler separately after initialization.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      // Set handler after Ditto.open()
      // The handler is async and receives the Ditto instance as its first parameter
      ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
              // Initial auth
              expiredDitto.auth?.login(
                  token: "your-token",
                  provider: .development
              ) { clientInfo, error in
                  if let error = error {
                      print("Ditto auth failed: \(error), client info: \(String(describing: clientInfo))")
                  }
              }
      }
      ```

      ```swift SWIFT (v4) theme={null}
      // Auth mixed with identity
      let ditto = Ditto(
          identity: .onlinePlayground(
              appID: "...",
              token: "...",  // Token in identity
              enableDittoCloudSync: false,
              customAuthURL: URL(...)
          )
      )
      ```
    </CodeGroup>

    **Key changes:**

    * Authentication now uses `expirationHandler` closure
    * The handler signature is `@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void`
    * Handler called when auth required (`timeUntilExpiration == 0`) or token expiring
    * Use `.development` provider for playground tokens
    * Custom auth: extend `DittoAuthenticationProvider` with a static property, or use `DittoAuthenticationProvider("your-provider")`
  </Step>

  <Step title="Start Sync">
    Start sync after initialization.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      try ditto.sync.start()
      ```

      ```swift SWIFT (v4) theme={null}
      try? ditto.startSync()
      ```
    </CodeGroup>

    **Key changes:**

    * Namespace change from `ditto.startSync()` to `ditto.sync.start()`
  </Step>
</Steps>

### Auth Handler: Swift 6 Actor Isolation Requirement

If your `DittoManager` (or whatever class holds the `Ditto` instance) is an `actor`,
the `expirationHandler` closure is `@Sendable` and executes off the actor. Any access
to actor-isolated state inside a `Task { }` block requires `await`:

```swift theme={null}
// BROKEN — Swift 6 strict concurrency error
ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
    if let error = someError {
        Task {
            self.myActorIsolatedProperty = error  // ← error: actor-isolated
        }
    }
}

// FIXED
ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
    if let error = someError {
        Task {
            await self.myActorIsolatedProperty = error  // ← correct
        }
    }
}
```

## Additional Changes

### DQL Strict Mode Behavior Change

<Warning>
  **Breaking Change**: v5 defaults to `DQL_STRICT_MODE=false`, which fundamentally changes how DQL queries behave.

  * **v4 default**: Objects treated as **REGISTER** (whole-object replacement)
  * **v5 default**: Objects treated as **MAP** (field-level merging)

  This affects the behavior of all DQL `SELECT`, `INSERT`, and `UPDATE` operations.
</Warning>

Choose the appropriate migration path based on your current v4 configuration:

<AccordionGroup>
  <Accordion title="Currently Using DQL with DQL_STRICT_MODE=true (v4 default)">
    **If you're currently using the v4 default (`DQL_STRICT_MODE=true`)**, you **must** explicitly set strict mode to `true` in v5 before starting sync or executing any queries.

    <Warning>
      Failing to set strict mode will cause objects to merge at the field level instead of replacing entirely, which can result in unexpected data behavior and perceived data loss.
    </Warning>

    <Note>
      To migrate to `DQL_STRICT_MODE=false` (the new v5 default), contact Ditto Customer Support for guidance.
    </Note>

    <CodeGroup>
      ```swift SWIFT (v5 - Maintain v4 Behavior) highlight={6,7} theme={null}
      let config = DittoConfig(
          databaseID: "your-database-id",
          connect: .server(url: URL(string: "https://your-server.ditto.live")!)
      )
      let ditto = try await Ditto.open(config: config)

      // IMPORTANT: Set strict mode BEFORE starting sync or running queries
      try await ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = true")

      // Now safe to start sync
      try ditto.sync.start()
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Currently Using DQL with DQL_STRICT_MODE=false">
    **If you explicitly set `DQL_STRICT_MODE=false` in v4**, no changes are required.

    v5 uses `DQL_STRICT_MODE=false` as the default, so your existing DQL queries will behave identically. You can upgrade freely.

    **Optional**: You can remove the explicit `ALTER SYSTEM SET DQL_STRICT_MODE = false` statement in v5 since this is now the default behavior.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      let config = DittoConfig(
          databaseID: "your-database-id",
          connect: .server(url: URL(string: "https://your-server.ditto.live")!)
      )
      let ditto = try await Ditto.open(config: config)

      // No need to set DQL_STRICT_MODE - false is now the default
      try ditto.sync.start()
      ```

      ```swift SWIFT (v4 - Your Current Setup) theme={null}
      let ditto = Ditto(
          identity: .onlinePlayground(
              appID: "your-app-id",
              token: "your-token",
              enableDittoCloudSync: false,
              customAuthURL: URL(string: "https://your-server.ditto.live")
          )
      )

      // You explicitly set this in v4
      try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")

      try? ditto.startSync()
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Currently Using Legacy Query Builder">
    The Legacy Query Builder has been removed in v5. All queries must be converted to DQL before upgrading.

    **Good news**: Legacy Query Builder functionality has 1:1 support with `DQL_STRICT_MODE=false` (the v5 default), making migrations straightforward.

    **Migration Steps**:

    1. **In v4: Set `DQL_STRICT_MODE=false`** after initialization:
           <CodeGroup>
             ```swift SWIFT (v4 - Enable Non-Strict Mode) theme={null}
             let ditto = Ditto(
                 identity: .onlinePlayground(
                     appID: "your-app-id",
                     token: "your-token",
                     enableDittoCloudSync: false,
                     customAuthURL: URL(string: "https://your-server.ditto.live")
                 )
             )

             // Set DQL_STRICT_MODE to false for Query Builder compatibility
             try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")

             try? ditto.startSync()
             ```
           </CodeGroup>

    2. **Convert all queries from Legacy Query Builder to DQL**
       See the [Swift Legacy→DQL Migration Guide](./swift-dql) for detailed conversion examples.

    3. **Upgrade to v5**
       No DQL configuration changes required—v5 defaults to `DQL_STRICT_MODE=false`.
  </Accordion>
</AccordionGroup>

For additional guidance or questions, contact Ditto Customer Support.

### Default Persistence Directory

v5 includes the database ID in the default directory name: `ditto-{databaseID}` instead of `ditto`.  This is for only new databases created in v5.  v5 does not migrate existing databases to the new structure.

**Provide custom persistence directory:**

```swift theme={null}
let config = DittoConfig(
    databaseID: "your-database-id",
    connect: .server(url: URL(...)),
    persistenceDirectory: URL(fileURLWithPath: "/app/sandbox/ditto")  // custom path to save the database 
)
```

### Observer Changes

v4 and v5 both use callback-based observers. The core `registerObserver` API pattern is the same in both versions, returning a `DittoStoreObserver` that you retain and call `.stop()` on for cleanup.

<CodeGroup>
  ```swift SWIFT (v5) theme={null}
  var observer: DittoStoreObserver?

  observer = try ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      let cars = result.items.compactMap { item -> Car? in
          let car = Car(item.jsonData())
          item.dematerialize()  // Free native memory — do not use item after this
          return car
      }
      DispatchQueue.main.async { self.cars = cars }
  }

  // Cleanup
  deinit {
      observer?.stop()
  }
  ```

  ```swift SWIFT (v4) theme={null}
  var observer: DittoStoreObserver?

  observer = ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      DispatchQueue.main.async {
          self.cars = result.items.compactMap { Car($0.jsonData()) }
      }
  }

  deinit {
      observer?.stop()
  }
  ```
</CodeGroup>

**Key changes:**

* v5 adds `Codable` argument overloads for `registerObserver`
* Call `item.dematerialize()` after extracting data from `DittoQueryResultItem` to free native memory
* The observer lifecycle pattern (retain, `.stop()`, cleanup) is the same as v4

### Back Pressure (Signal Next)

The standard `registerObserver` automatically signals readiness for the next callback after your handler returns. For handlers that need time to process results — such as expensive rendering or batch operations — use `registerObserver(query:arguments:deliverOn:handlerWithSignalNext:)` to control when the next callback is delivered.

<Note>
  **Coalescing behavior**: While your handler is processing, Ditto coalesces any
  intermediate changes. When you call `signalNext()`, you receive a single
  callback with the latest state — not every intermediate change.
</Note>

```swift theme={null}
// Standard observer — signalNext called automatically after handler returns
let observer = try ditto.store.registerObserver(
    query: "SELECT * FROM cars WHERE color = :color",
    arguments: ["color": "blue"]
) { result in
    updateUI(result.items)
    // signalNext() called automatically here
}

// Back-pressure observer — you control when the next callback fires
let observer = try ditto.store.registerObserver(
    query: "SELECT * FROM cars WHERE color = :color",
    arguments: ["color": "blue"],
    handlerWithSignalNext: { result, signalNext in
        Task {
            await expensiveRenderOperation(result.items)
            signalNext()  // Ready for next update
        }
    }
)
```

**When to use signal next:**

* Processing updates is computationally expensive
* You're performing async work (network calls, heavy rendering) on each update
* Query results change very frequently and you want to process only the latest state

### DittoDiskUsageItem Property Names (v5, Unchanged)

`DiskUsageItem` was renamed to `DittoDiskUsageItem`, however, the property names
are **unchanged** in v5:

* `.path: String` — unchanged
* `.sizeInBytes: Int` — unchanged
* `.childItems: [DittoDiskUsageItem]` — unchanged (type renamed, property name same)

### `DittoConnection` Property Renames

| v4.14                                    | v5.0                         |
| ---------------------------------------- | ---------------------------- |
| `connection.peerKeyString1`              | `connection.peer1`           |
| `connection.peerKeyString2`              | `connection.peer2`           |
| `connection.approximateDistanceInMeters` | **REMOVED — no replacement** |

### DittoPeer Property Renames (v5)

| v4.14                          | v5.0                            |
| ------------------------------ | ------------------------------- |
| `peer.peerKeyString`           | `peer.peerKey`                  |
| `peer.osV2: DittoPeerOS?`      | `peer.os: DittoPeerOS?`         |
| `peer.isConnectedToDittoCloud` | `peer.isConnectedToDittoServer` |

### **Breaking**: DittoConnection Conformances Cause Swift 6 `filter` Ambiguity

In v5, `DittoConnection` gains `Identifiable`, `Equatable`, `Hashable`, and `Sendable` conformances.
In Swift 6, `Foundation.filter(_:Predicate)` takes precedence over `Sequence.filter(_:)` when the
element type conforms to these protocols, causing an unexpected compile error.

**Error:** `trailing closure passed to parameter of type 'Predicate<DittoConnection>'`

**Fix:** Annotate the closure parameter type explicitly:

```swift theme={null}
// BROKEN in Swift 6
connections.filter { $0.peer1 == localKey }

// FIXED
connections.filter { (conn: DittoConnection) in conn.peer1 == localKey }
```

### DittoLogger Changes

`DittoLogger` retains the same class name in both v4 and v5. The key changes are:

* v5 marks `DittoLogger` as `final` and `Sendable` for Swift concurrency safety
* `DittoLogger.setLogFile()` is removed in v5 (was deprecated in v4 — use `export(to:)` instead)

### API Changes

| v4.14                                                                | v5.0                                                                     |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `ditto.startSync()`                                                  | `ditto.sync.start()`                                                     |
| `ditto.stopSync()`                                                   | `ditto.sync.stop()`                                                      |
| `ditto.isSyncActive`                                                 | `ditto.sync.isActive`                                                    |
| `ditto.observePeers(_:)` / `observePeersV2(_:)`                      | `ditto.presence.observe(didChangeHandler:)`                              |
| `ditto.activated`                                                    | `ditto.isActivated`                                                      |
| `peerKeyString`                                                      | `peerKey`                                                                |
| `isConnectedToDittoCloud`                                            | `isConnectedToDittoServer`                                               |
| `Ditto(identity:)`                                                   | `Ditto.open(config:)` (async) or `Ditto.openSync(config:)`               |
| `ditto.siteID`                                                       | Not a standalone property — access via `ditto.presence.graph.localPeer`  |
| `ditto.appID`                                                        | `ditto.config.databaseID`                                                |
| `DiskUsage` class                                                    | `DittoDiskUsage` class                                                   |
| `ditto.diskUsage` (typed `DiskUsage`)                                | `ditto.diskUsage` (now typed `DittoDiskUsage`)                           |
| `DiskUsage.exec: DiskUsageItem`                                      | `DittoDiskUsage.item: DittoDiskUsageItem`                                |
| `DiskUsage.DiskUsageObserverHandle` (nested)                         | `DittoDiskUsageObserver` (standalone class)                              |
| `DiskUsage.observe(eventHandler:)` returns `DiskUsageObserverHandle` | `DittoDiskUsage.observe(eventHandler:)` returns `DittoDiskUsageObserver` |
| `DiskUsageItem`                                                      | `DittoDiskUsageItem`                                                     |
| `DittoSwiftError` typealias                                          | Removed — use `DittoError` directly                                      |
| `DittoError.migrationError(reason:)`                                 | Removed                                                                  |
| `MigrationErrorReason` enum                                          | Removed                                                                  |
| `DittoSmallPeerInfo.syncScope: DittoSmallPeerInfoSyncScope`          | DQL: `ALTER SYSTEM SET sync_scope = '...'`                               |
| `DittoSmallPeerInfoSyncScope` enum                                   | Removed — configure via DQL                                              |
| `DittoSmallPeerInfo.metadataJSONString`                              | `DittoSmallPeerInfo.metadataJSONData`                                    |
| `DittoSmallPeerInfo.setMetadataJSONString(_:)`                       | `DittoSmallPeerInfo.setMetadataJSONData(_:)`                             |
| `DittoLogger.setLogFile()`                                           | Removed — use `export(to:)` instead                                      |

### WebSocket Sync Default Changed

<Warning>
  `DittoHTTPListenConfig.webSocketSync` default changed from `true` to `false`.
  If your app relied on the v4 default, you must now explicitly enable it.
</Warning>

### Sync Subscription Changes

`DittoSync.subscriptions` is now computed rather than a stored Swift `Set`. Do not cache this collection — query it fresh when needed. The same applies to `DittoStore.observers`.

***

## APIs Removed

These v4 APIs were removed

| Removed API                                                     | Notes                                   |
| --------------------------------------------------------------- | --------------------------------------- |
| `Ditto.transportDiagnostics()` / `DittoTransportDiagnostics`    | Low-level transport debug info removed  |
| `Ditto.runGarbageCollection()`                                  | GC is now handled automatically         |
| `Ditto.disableSyncWithV3()`                                     | v3 protocol support fully dropped       |
| `Ditto.isHistoryTrackingEnabled` / history tracking initializer | History tracking removed entirely       |
| `DittoIdentity.manual(certificateConfig:)`                      | Manual certificate identity removed     |
| `DittoConnection.approximateDistanceInMeters`                   | Peer distance estimation removed        |
| `DittoHTTPListenConfig.staticContentPath`                       | Static HTTP content serving removed     |
| `DittoStore.queriesHash(queries:)`                              | Query set hashing removed               |
| `DittoStore.queriesHashMnemonic(queries:)`                      | Query mnemonic hashing removed          |
| `DittoConnectionPriority` enum                                  | Connection priority control removed     |
| `DittoTransportSnapshot` struct                                 | Removed with transport diagnostics      |
| `DittoRemotePeer` struct (deprecated)                           | Use `DittoPeer` in `DittoPresenceGraph` |
| `DittoAddress` struct                                           | Removed                                 |
| `DittoExperimental.jsonByTranscoding(cbor:)`                    | CBOR-to-JSON utility removed            |
| `DittoLogger.setLogFile()`                                      | Use `export(to:)` instead               |

***

## New APIs in v5

* **`Ditto.open(config:): async throws -> Ditto`** — async factory method
* **`Ditto.openSync(config:): throws -> Ditto`** — synchronous factory alternative
* **`ditto.config: DittoConfig`** — read the config used to open this instance
* **`ditto.isActivated: Bool`** — replaces `ditto.activated`
* **`DittoStore.newAttachment(data: Data, metadata:)`** — create attachment from in-memory `Data` (v4 was file path only)
* **`DittoStore.registerObserver(query:arguments: some Codable, ...)`** — overloads accepting `Codable` query arguments
* **`DittoSync.registerSubscription(query:arguments: some Codable)`** — overload accepting `Codable` arguments
* **`DittoDiskUsageObserver`** — standalone observer class (was nested `DiskUsage.DiskUsageObserverHandle`)
* **`DittoDiskUsageItem`** — renamed from `DiskUsageItem`
* **`DittoSyncSubscription.queryArgumentsCBORData`** / **`.queryArgumentsJSONData`** / **`.id`** — new properties for introspection
* **`DittoConnection`** now conforms to `Identifiable`, `Equatable`, `Hashable`, and `Sendable`
* All public types now `final` and `Sendable` for Swift concurrency
* `DittoConfig` and `DittoConfigConnect` now conform to `Sendable` and `Codable`

***

## Migration Checklist

### Initialization

* [ ] Replace `Ditto(identity:)` with `Ditto.open(config:)`
* [ ] Create `DittoConfig` with `databaseID` and `connect` mode
* [ ] Add `try await` for async initialization
* [ ] Update `.onlinePlayground` → `.server(url:)`
* [ ] Update `.offlinePlayground` → `.smallPeersOnly(privateKey: nil)`
* [ ] Remove `updateTransportConfig` calls
* [ ] Remove `disableSyncWithV3()` calls
* [ ] **Set `DQL_STRICT_MODE=true` BEFORE starting sync if maintaining v4 behavior**
* [ ] Update `startSync()` → `sync.start()`

### Authentication

* [ ] Set `expirationHandler` closure after initialization
* [ ] Use `.development` provider for playground tokens
* [ ] Remove authentication from identity configuration

### Observers

* [ ] Call `item.dematerialize()` after extracting data from `DittoQueryResultItem` to free native memory
* [ ] If processing is expensive, use `handlerWithSignalNext:` for back-pressure control
* [ ] v5 adds `Codable` argument overloads — adopt where convenient

### Data Operations

* [ ] Replace all `.collection("x").find(...)` chains with `store.execute()` DQL
* [ ] Use parameterized queries with `:paramName` — never string interpolation
* [ ] Replace `.upsert()` with `INSERT INTO ... ON ID CONFLICT DO UPDATE`
* [ ] Replace `.update {}` closures with `UPDATE SET` DQL
* [ ] Replace `.remove()` with `DELETE FROM`
* [ ] Replace `.evict()` with `EVICT FROM`
* [ ] Replace `.observeLocal {}` with `store.registerObserver()` callback
* [ ] Replace `.subscribe()` with `sync.registerSubscription()`
* [ ] Replace `counter?.increment(by:)` with `UPDATE APPLY ... PN_INCREMENT BY`

### Breaking Changes

* [ ] Set `persistenceDirectory` if maintaining v4 directory structure
* [ ] Update `peerKeyString` → `peerKey`
* [ ] Update `diskUsage` usage: `DiskUsage` → `DittoDiskUsage`, `.exec` → `.item`, `DiskUsageObserverHandle` → `DittoDiskUsageObserver`
* [ ] Update `DittoConnection.peer1`/`peer2` comparisons: now `String` instead of `Data`
* [ ] Update `isConnectedToDittoCloud` → `isConnectedToDittoServer`
* [ ] Update `ditto.observePeers(_:)` → `ditto.presence.observe(didChangeHandler:)`
* [ ] Update `ditto.activated` → `ditto.isActivated`
* [ ] Remove all `DittoSwiftError` references — use `DittoError` directly
* [ ] Update `DittoSmallPeerInfo.metadataJSONString` → `metadataJSONData`
* [ ] Check for explicit `webSocketSync = true` if your app relied on v4 default
* [ ] Update `DittoLogger.setLogFile()` calls — use `export(to:)` instead

### Verification

* [ ] Build compiles with zero errors
* [ ] No deprecated API warnings
* [ ] Observers update UI immediately
* [ ] Authentication works before sync starts
* [ ] No memory leaks on navigation
* [ ] No memory leaks (Instruments: Allocations, Leaks)

***

## Common Pitfalls

1. **DQL\_STRICT\_MODE silent change**: Not setting `DQL_STRICT_MODE=true` when your v4 app used the default. Objects that were replaced whole will now merge at field level — causes unexpected data merging.

2. **Forgetting `try await`**: `Ditto.open(config:)` is async. Not awaiting it causes a compile error.

3. **Storing `DittoQueryResultItem` outside callback**: These hold native memory. Always call `dematerialize()` and extract your model before the callback returns.

4. **String interpolation in queries**: Never `"SELECT * FROM cars WHERE color = '\(color)'"`. Always use `arguments: ["color": color]`.

5. **Counter initialization**: Don't put `DittoCounter()` or `0` in insert documents. Counters are created on first `PN_INCREMENT`. Inserting `0` creates a REGISTER, not a COUNTER.

6. **Missing `ON ID CONFLICT`**: INSERT fails if document `_id` already exists without a conflict clause.

7. **Not checking `timeUntilExpiration`** in auth handler: Handler is called for both initial auth (0) and token refresh (>0). Handle both cases.
