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

# Kotlin V4→V5 API Migration Guide

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

## Overview

This guide covers the essential changes needed to migrate your Ditto Kotlin Android app from v4 to v5. The main architectural shift is moving from identity-based initialization to a three-phase configuration model with Flow-based APIs.

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

***

## AI Agent Prompt

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

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

  SDK PACKAGE RENAME:
  Replace Ditto SDK imports under `live.ditto.*` with their `com.ditto.kotlin.*` equivalents. Do not run a blind project-wide replacement if the application also uses DittoAndroidTools: tools imports move from `live.ditto.tools.*` to `com.ditto.tools.*`, not `com.ditto.kotlin.tools.*`.

  You must replace your definition in your gradle file(s):

  ```kotlin
  //v5 — use the KMP root module; Gradle auto-resolves the Android variant, no -android suffix needed
  ditto-kotlin = { module = "com.ditto:ditto-kotlin", version.ref = "ditto" }
  ```

  ```kotlin
  //v4
  live-ditto = { group = "live.ditto", name = "ditto", version.ref = "ditto" }
  ```

  Also update the `implementation(libs.live.ditto)` dependency reference in your app's `build.gradle.kts`:

  ```kotlin
  // v5
  implementation(libs.ditto.kotlin)
  // v4
  implementation(libs.live.ditto)
  ```

  Full import mapping:

  - `live.ditto.*` → `com.ditto.kotlin.*`
  - `live.ditto.transports.*` → `com.ditto.kotlin.transports.*` (Note: `DittoSyncPermissions` is now in `com.ditto.kotlin.transports`)
  - `live.ditto.android.*` → `com.ditto.kotlin.*` (Android-specific classes merged into main package)
  - NEW: `com.ditto.kotlin.serialization.*` for `DittoCborSerializable`, `DittoJsonSerializable`, and `toDittoCbor()` extensions
  - Replace SDK imports by package family. Handle `live.ditto.tools.*` separately as described in DITTO TOOLS below.

  Minimum Supported Android Version:

  - Ditto SDK v5 requires `minSdk = 24` (Android 7.0 Nougat). If your project targets `minSdk = 23`, you must raise it in `build.gradle.kts`.

  DittoInitializer (Android context setup):

  - v5 automatically handles Android context via AndroidX App Startup through the SDK's own manifest merger. You do NOT need to manually add DittoInitializer to your AndroidManifest.xml. Remove any manual DittoInitializer manifest entries if present.

  INITIALIZATION:
  Replace `Ditto(dependencies, identity)` constructor and `Ditto.open(DittoAndroidConfig)` with `DittoFactory.create(DittoConfig(...))`

  - `DittoFactory.create()` is NOT suspend — no coroutine needed
  - `DittoConfig(databaseId, connect, ...)` replaces `DittoAndroidConfig(context, databaseId, connect, ...)`  — no `Context` parameter; Android context handled internally via `DittoInitializer` (AndroidX Startup)
  - `DittoConfig.Connect.Server(url: String)` replaces `DittoConfigConnect.Server(url: URI)` — URL is now a `String`, not `java.net.URI`; class is nested inside `DittoConfig`. Copy the URL from the Ditto Portal (Connect via SDK → **URL**) as-is — don't build it from the database ID. This is the portal's **URL** value — an `https://` URL. v5 takes no separate WebSocket (`wss://`) URL; do not paste a `wss://` value here.
  - `DittoConfig.Connect.SmallPeersOnly(privateKey?)` replaces `DittoConfigConnect.SmallPeersOnly(privateKey?)`
  - `DittoConfig` properties are now `val` (immutable); `persistenceDirectory` is `String?` instead of `File?`
  - Remove `DefaultAndroidDittoDependencies`, `AndroidDittoDependencies`, `DittoDependencies`, `DittoBase` — all removed
  - Remove `DittoIdentity` sealed class and all subclasses (`OnlinePlayground`, `OnlineWithAuthentication`, `OfflinePlayground`, `SharedKey`, `Manual`) — all removed
  - Remove `UpdateTransportConfig` websocket URL additions (cloud URL inferred from `DittoConfig.Connect.Server.url`)
  - Remove `DisableSyncWithV3()`, `RunGarbageCollection()`, `GetTransportDiagnostics()` — all removed
  - Remove `DQL_STRICT_MODE = false` workaround (now the v5 default)
  - If your v4 app relied on `DQL_STRICT_MODE = true` (v4 default), you MUST set it before `sync.start()`:
    `ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = true")`

  SYNC:

  - `ditto.startSync()` → `ditto.sync.start()`
  - `ditto.stopSync()` → `ditto.sync.stop()`
  - `ditto.isSyncActive` → `ditto.sync.isActive`
  - `ditto.sync.registerSubscription(query, args)` — args can be `Map<String, Any?>` (plain map) or `DittoCborSerializable.Dictionary`

  AUTHENTICATION — CRITICAL SYNTAX RULES:
  `auth.expirationHandler` is a Kotlin PROPERTY. You MUST use property assignment (`=`), NOT a method call.
  There is NO `setExpirationHandler()` method — it does not exist and will not compile.
  The lambda IS a suspend lambda — you call `auth.login()` directly inside it without launching a new coroutine.

  CORRECT syntax:

  ```kotlin
  ditto.auth?.let { auth ->
      auth.expirationHandler = { ditto, timeUntilExpiration ->
          // This lambda body IS suspend — call login() directly here, no coroutine needed
          ditto.auth?.login(token = "your-token", provider = DittoAuthenticationProvider.development())
      }
  }
  ```

  FORBIDDEN patterns that will NOT compile or will fail at runtime:

  ```kotlin
  // ERROR: No such method exists
  auth.setExpirationHandler { ditto, t -> ... }

  // ERROR: Missing `=`; this calls the property as a function
  auth.expirationHandler { ditto, t -> ... }

  // WRONG: Never launch a new coroutine inside the handler — the lambda IS already suspend
  auth.expirationHandler = { ditto, t ->
      CoroutineScope(Dispatchers.IO).launch { ditto.auth?.login(...) }
  }
  ```

  Other authentication rules:

  - Handler receives `(ditto: Ditto, timeUntilExpiration: Double)` — note: param is now `Ditto`, not `DittoBase`
  - Called when `timeUntilExpiration == 0.0` (initial auth or expired) or `> 0` (token expiring soon — refresh)
  - `ditto.auth` is nullable — use `?.let { }` guard; it is null when using `DittoConfig.Connect.SmallPeersOnly`
  - You MUST set `expirationHandler` BEFORE calling `ditto.sync.start()` when using `DittoConfig.Connect.Server`; failing to set it causes `DittoException.AuthenticationException` with reason `ExpirationHandlerMissing`
  - Use `DittoAuthenticationProvider.development()` for playground tokens
  - Use `DittoAuthenticationProvider.custom("provider-name")` for custom providers
  - `auth.login(token, provider)` is now suspend — no callback parameter
  - `auth.observeStatus()` returns `StateFlow<DittoAuthenticationStatus>` — was callback-based
  - `auth.logout(cleanupFn: ((Ditto) -> Unit)?)` — param type changed from `DittoBase` to `Ditto`
  - Remove `DittoAuthenticationCallback`, `DittoLoginCallback`, `DittoLoginCompletionCallback`, `DittoAuthenticationStatusDidChangeCallback`, `DittoLogoutCleanupFn` — all removed
  - Remove `loginWithToken()`, `loginWithCredentials()` — deprecated and removed

  STORE EXECUTE — CRITICAL BREAKING CHANGE FOR READS:
  In v4, `store.execute(query, args)` returned `DittoQueryResult` directly.
  In v5, `store.execute(query, args)` returns NOTHING (Unit). Calling `.items` on the result IS A COMPILE ERROR.

  FORBIDDEN v4 pattern that will NOT compile in v5:

  ```kotlin
  // ERROR: store.execute() returns Unit in v5, not DittoQueryResult
  val item = store.execute("SELECT * FROM tasks WHERE _id = :id", mapOf("_id" to id)).items.first()
  ```

  CORRECT v5 patterns for reads:

  ```kotlin
  // Option A — handler form (RECOMMENDED): result is auto-closed after lambda returns
  val item = ditto.store.execute(
      query = "SELECT * FROM tasks WHERE _id = :id",
      arguments = mapOf("_id" to id)
  ) { result -> result.items.firstOrNull() }

  // Option B — executeRaw: same as v4 behavior, but YOU must close the result
  val result = ditto.store.executeRaw("SELECT * FROM tasks WHERE _id = :id", mapOf("_id" to id))
  val item = result.items.firstOrNull()
  result.close()  // REQUIRED — resource leak if omitted
  ```

  For WRITES (INSERT/UPDATE/DELETE), use the no-handler overload — it still suspends until the query finishes, just without a `DittoQueryResult`:

  ```kotlin
  // Correct for writes — returns nothing, that's expected
  ditto.store.execute("UPDATE tasks SET done = :done WHERE _id = :id", mapOf("done" to true, "id" to id))
  ```

  Other store.execute rules:

  - Replace `store.execute(query, args): DittoQueryResult` with new overloads:
    - Fire-and-forget (writes): `store.execute(query, args)` — discards result
    - With handler (reads): `store.execute(query, args) { result -> result.items.toList() }` — result auto-closed
    - Raw (manual): `store.executeRaw(query, args): DittoQueryResult` — caller must close
  - All are `suspend` functions
  - Arguments accept `Map<String, Any?>` (plain Kotlin map — recommended), `DittoCborSerializable.Dictionary`, or `@Serializable` types
  - Remove `store.executeBlocking()` — use `runBlocking { store.execute() }` if needed
  - `DittoQueryResultItem.value` is now `DittoCborSerializable.Dictionary` instead of `Map<String, Any>` — the cast `doc.value["field"] as SomeType` will throw at runtime; use typed accessors: `.stringOrNull`, `.intOrNull`, `.longOrNull`, `.doubleOrNull`, `.booleanOrNull`, `.listOrNull`, `.dictionaryOrNull`, `.attachmentTokenOrNull`, `.isNull`
  - Read a named scalar by indexing the dictionary with a String key, then chaining the accessor: `item.value["counter"].intOrNull` (the accessors are properties on the looked-up `DittoCborSerializable`). The `Dictionary` has a `get(key: String)` overload — you do not need to wrap the key or round-trip through `jsonString()`. Use `jsonString()` only for whole nested objects/arrays. Note the String-key subscript returns a non-null `DittoCborSerializable`, so for a possibly-absent field it's the `.intOrNull` accessor — not the subscript — that yields `null`.
  - Or use `item.jsonString()` to get a JSON string and deserialize from JSON (recommended for complex objects)

  OBSERVERS — CRITICAL MEMORY RULE:
  The `DittoQueryResult` passed to your handler/transform is a managed native resource. As soon as the handler returns (or the next event arrives), it is closed and its `DittoQueryResultItem`s are released. **Never** return `result.items` (or anything that references its items) out of the lambda — doing so will throw `IllegalStateException: Query result item has already been closed`. Inside the lambda, convert each item to plain Kotlin data via `item.jsonString()` (decode with kotlinx-serialization) or typed accessors on `item.value` (e.g. `item.value["color"].string`, `.stringOrNull`).

  Three patterns replace the v4 callback model:

  1. `registerObserver(query, args) { result -> ... }` — handler is now `suspend`; returns `DittoStoreObserver` (manual `close()`); supports optional `DittoDiff` second parameter via the `(result, diff) -> ...` overload
  2. `observe(query, args) { result -> transform }` — NEW; returns cold `Flow<T>`; cancelled with the collecting scope (e.g. `lifecycleScope`); transform MUST return plain Kotlin data, not a `DittoQueryResult` or its items
  3. `collect(query, args) { result -> ... }` — NEW; suspend function; provides natural back-pressure because Ditto only signals the next event after the handler returns

  BACK-PRESSURE — IMPORTANT:
  `registerObserver()` and `collect()` provide back-pressure automatically (the suspend handler gates the next event). `observe()` does NOT — its Flow signals Ditto for the next event right after `emit` succeeds, so a slow collector can be flooded. If you need rendezvous-style back-pressure on a Flow returned by `observe()`, add an explicit `.buffer(Channel.RENDEZVOUS)` (every event matters) or `.conflate()` (only the latest snapshot matters). If you need natural back-pressure, prefer `collect()` over `observe()`.

  - Replace `runOnUiThread { ... }` with `withContext(Dispatchers.Main) { ... }` inside suspend handlers
  - Replace `DittoSignalNext` / `signalNext` back-pressure with `collect()`
  - Remove `DittoChangeHandler`, `DittoChangeHandlerWithNextSignal`, `DittoSignalNext`, `DittoSignalNextCallback` — all removed
  - When using `observe().collect { ... }` in `lifecycleScope`, the upstream observer is cancelled with the scope — no manual `.close()` needed

  LEGACY COLLECTION API — COMPLETELY REMOVED:
  All collection-based query APIs are removed. Use DQL queries via `store.execute()` instead:

  - `store.collection("x").find(query).exec()` → `store.execute("SELECT * FROM x WHERE ...", args) { result -> result.items }`
  - `store.collection("x").upsert(value)` → `store.execute("INSERT INTO x DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE", mapOf("doc" to value))`
  - `store.collection("x").findById(id).update { doc -> doc["field"].set(val) }` → `store.execute("UPDATE x SET field = :val WHERE _id = :id", mapOf("val" to val, "id" to id))`
  - `store.collection("x").findById(id).remove()` → `store.execute("DELETE FROM x WHERE _id = :id", mapOf("id" to id))`
  - `store.collection("x").findById(id).evict()` → `store.execute("EVICT FROM x WHERE _id = :id", mapOf("id" to id))`
  - Counter: `doc["count"].counter.increment(1.0)` → `store.execute("UPDATE x APPLY count PN_INCREMENT BY :n WHERE _id = :id", mapOf("n" to 1, "id" to id))`
  - `store.collection("x").find(q).observeLocal { docs, event -> ... }` → `store.observe(dqlQuery, args) { result -> ... }.collect { ... }`
  - `store.collection("x").find(q).subscribe()` → `ditto.sync.registerSubscription(dqlQuery, args)`
  - Remove: `DittoCollection`, `DittoDocument`, `DittoMutableDocument`, `DittoDocumentId`, `DittoDocumentPath`, `DittoDocumentIdPath`, `DittoMutableDocumentPath`, `DittoMutableCounter`, `DittoMutableRegister`, `DittoPendingCursorOperation`, `DittoPendingIdSpecificOperation`, `DittoPendingCollectionsOperation`, `DittoLiveQuery`, `DittoLiveQueryEvent`, `DittoSingleDocumentLiveQueryEvent`, `DittoSubscription` (legacy), `DittoWriteTransaction`, `DittoScopedWriteTransaction`, `DittoWriteTransactionResult`, `DittoUpdateResult`, `DittoWriteStrategy`, `DittoSortDirection`, `DittoCollectionsEvent`, `DittoLiveQueryMove`

  TRANSACTIONS:

  - `store.transaction { txn -> DittoTransactionCompletionAction.Commit }` → `store.transaction { txn -> DittoTransaction.Result.Commit(value) }` — now generic, returns a value
  - `DittoTransactionCompletionAction.Abort` → `DittoTransaction.Result.Rollback`
  - `store.write(block)` → `store.transaction { ... }` — write transactions removed
  - Remove `transactionBlocking()`, `transactionReturningVoid()` — use `runBlocking { store.transaction {} }` if needed

  TRANSPORT CONFIG:
  Changed from mutable data classes to immutable builder pattern:

  - `ditto.updateTransportConfig { config -> config.peerToPeer.bluetoothLe.enabled = true }` →
    `ditto.updateTransportConfig { config -> config.peerToPeer { bluetoothLe { enabled = true } } }`
  - Builder uses DSL-style nested blocks
  - `enableAllPeerToPeer()` — removed; configure each transport explicitly
  - `DittoAdvertisementPower`: `VERY_LOW`→`VeryLow`, `LOW`→`Low`, `MEDIUM`→`Medium`, `HIGH`→`High`
  - `DittoAdvertisementFrequency`: `LOW`→`Low`, `MEDIUM`→`Medium`, `HIGH`→`High`
  - `DittoBluetoothLeConfig.maxOutgoing`, `.mtuRequest` — removed
  - `DittoHttpListenConfig.staticContentPath` — removed
  - `DittoGlobalConfig.syncGroupLong`, `.routingHintLong` — removed; use `UInt` directly
  - Nested class renames: `DittoPeerToPeer`→`DittoTransportConfig.PeerToPeer`, `DittoBluetoothLeConfig`→`PeerToPeer.BluetoothLE`, `DittoLanConfig`→`PeerToPeer.Lan`, `DittoWifiAwareConfig`→`PeerToPeer.WifiAware`, `DittoConnect`→`Connect`, `DittoListen`→`Listen`, `DittoTcpListenConfig`→`Listen.TcpConfig`, `DittoHttpListenConfig`→`Listen.HttpConfig`, `DittoGlobalConfig`→`Global`

  TRANSPORT CONDITION:

  - `ditto.callback = object : DittoCallback { override fun transportConditionDidChange(...) }` →
    `ditto.transportCondition.collect { event -> /* event.condition, event.subsystem */ }`
  - `ditto.transportCondition` is a `Flow<DittoTransportConditionEvent>`
  - Remove `DittoCallback`, `DittoTransportConditionChangedCallback` — replaced by Flow

  PRESENCE:

  - `presence.observe(handler)` returning `DittoPresenceObserver` → `presence.observe()` returning `Flow<DittoPresenceGraph>` — collect in `lifecycleScope`
  - `DittoPresenceGraph.json()` → `DittoPresenceGraph.serializeToJson()`
  - `presence.peerMetadata: Map<String, Any?>` → `presence.peerMetadata: DittoJsonSerializable.ObjectValue` — use `getPeerMetadataMap()` for plain Map or `setPeerMetadata(Map)` to set
  - `presence.connectionRequestHandlerJava` — removed; use `presence.connectionRequestHandler` fun interface
  - `DittoConnectionRequest.peerKeyString` → `DittoConnectionRequest.peerKey`
  - Remove `DittoPresenceObserver`, `DittoPresenceObserverCallback`, `DittoPeersObserver` — use Flow

  <Warning>
  **Android v4 migration note:** In v4, if a `PresenceObserver` created via `presence.observe(handler)` was not explicitly closed, Android's garbage collector would eventually finalize it. However, if a custom log callback was registered (via `setCustomLogCallback`), the finalizer's teardown could trigger I/O on the `FinalizerDaemon` thread, exceeding Android's 10-second timeout and causing a `java.util.concurrent.TimeoutException` crash.

  In v5, `presence.observe()` returns a `Flow<DittoPresenceGraph>`. Collect it within `lifecycleScope` so that cancellation and cleanup happen automatically when the lifecycle ends. This eliminates the risk of orphaned observers.

  </Warning>

  PEER & CONNECTION:

  - `DittoPeer.peerKeyString` → `DittoPeer.peerKey`
  - `DittoPeer.peerKey: DittoPeerKey` (ByteArray) — removed; use `peerKey: String`
  - `DittoPeer.isConnectedToDittoCloud` → `DittoPeer.isConnectedToDittoServer`
  - `DittoPeer.os: String?` → `DittoPeer.os: DittoPeerOs?` (enum: `Generic`, `Ios`, `Tvos`, `Android`, `Linux`, `Windows`, `MacOS`)
  - `DittoPeer.peerMetadata: Map<String, Any?>` → `DittoJsonSerializable.ObjectValue`
  - `DittoPeer.identityServiceMetadata: Map<String, Any?>` → `DittoJsonSerializable.ObjectValue`
  - `DittoPeer.queryOverlapGroup` — removed
  - `DittoConnection.peer1/peer2` simplified to `String` (peer key); `approximateDistanceInMeters` removed
  - Remove `DittoPeerKey` typealias, `toKeyString()`, `decodePeerKey()`, `DittoRemotePeer`

  ATTACHMENTS:

  - `store.fetchAttachment(token, callback)` → `suspend store.fetchAttachment(token, onFetchProgress?)` — now suspend, returns `DittoAttachmentFetchResult`
  - `DittoAttachmentFetchEvent` (Progress/Completed/Deleted) → `DittoAttachmentFetchResult` (Completed/Deleted); progress via `onFetchProgress: suspend (ULong, ULong) -> Unit` parameter
  - `store.newAttachment(path, metadata)` — signature accepts `Map<String, Any?>` or `DittoCborSerializable.Dictionary`, but the DQL `INSERT INTO … ATTACHMENT` layer currently only accepts string-valued metadata; non-String values fail at insert time with `attachment property 'metadata' is invalid`. Stick to `Map<String, String>` for now.
  - Read the completed attachment's bytes via FUNCTIONS, not properties: `attachment.getInputStream().readBytes()` or `attachment.getData()` (returns `ByteArray`). `attachment.data` / `attachment.inputStream` do NOT resolve as Kotlin properties and will not compile.
  - Remove `DittoAttachmentFetchEventType` enum, `DittoCollection.newAttachment()`, `DittoCollection.fetchAttachment()` (deprecated)

  ERROR HANDLING:

  - `DittoError` sealed class → `DittoException` sealed class
  - All subclasses renamed: `DittoError.StoreError` → `DittoException.StoreException`, `AuthenticationError` → `AuthenticationException`, `FatalError` → `FatalException`, etc.
  - All reason classes renamed: `StoreErrorReason` → `StoreExceptionReason`, `AuthenticationErrorReason` → `AuthenticationExceptionReason`, etc.
  - Removed deprecated: `FilesystemError`, `InternalError`, `LockedWorkingDirectoryError`
  - NEW exception types: `SmallPeerInfoException`, `DataStreamsException`, `DittoPanicException`

  LOGGING:

  - `DittoLogger.enabled` → `DittoLogger.isEnabled`
  - `DittoLogLevel.DEBUG` → `DittoLogLevel.Debug` (PascalCase for all: `Error`, `Warning`, `Info`, `Debug`, `Verbose`)
  - `DittoLogger.setCustomLogCallback(callback)` / `unsetCustomLogCallback()` → `DittoLogger.observeLogEvents(): Flow<DittoLogEvent>` — collect in coroutine scope
  - `DittoLogger.exportToFileBlocking()` return type changed from `BigInteger` to `ULong`
  - Remove `DittoLogger.emojiLogLevelHeadingsEnabled`, `DittoLogCallback` interface

  DISK USAGE:

  - `DiskUsage` → `DittoDiskUsage`; `DiskUsageItem` → `DittoDiskUsageItem`; `FileSystemType` → `DittoFileSystemType`
  - `diskUsage.exec` → `diskUsage.item`
  - `DiskUsageItem.sizeInBytes: Int` → `DittoDiskUsageItem.sizeInBytes: Long`
  - `diskUsage.observe(handler): Closeable` → `diskUsage.observe(): Flow<DittoDiskUsageItem>`
  - `FileSystemType.DIRECTORY/FILE/SYMLINK` → `DittoFileSystemType.Directory/File/Symlink`
  - Remove `DiskUsageCallback` interface

  OTHER DITTO CLASS CHANGES:

  - `ditto.appId` → removed (use `DittoConfig.databaseId`)
  - `ditto.siteId` → removed
  - `ditto.persistenceDirectory` → removed (use `ditto.absolutePersistenceDirectory`)
  - `ditto.sdkVersion` → removed (use `Ditto.VERSION` companion const)
  - `ditto.activated` → `ditto.isActivated`
  - `ditto.observePeers()` / `observePeersV2()` → removed (use `ditto.presence.observe()`)
  - `ditto.smallPeerInfo.syncScope` → removed; `DittoSmallPeerInfoSyncScope` enum removed
  - Classes implement `DittoResource` (extends `AutoCloseable`) instead of `java.io.Closeable`

  DITTO TOOLS (only if the project uses DittoAndroidTools):
  - Replace `live.ditto:ditto-tools-android` with `com.ditto:ditto-tools-android:6.0.0`.
  - Replace tools imports from `live.ditto.tools.*` with `com.ditto.tools.*`.
  - DittoAndroidTools 6 requires `com.ditto:ditto-kotlin` 5.0.1 or a later 5.x release and Android API 24 or later.
  - Replace `DittoHeartbeatInfo.peerKeyString` with `peerKey`.

  BEFORE (V4):

  ```kotlin
  import live.ditto.*
  import live.ditto.android.*
  import live.ditto.transports.*

  // Initialize
  val androidDeps = DefaultAndroidDittoDependencies(context)
  val ditto = Ditto(
      androidDeps,
      DittoIdentity.OnlinePlayground(
          dependencies = androidDeps,
          appId = "my-app-id",
          token = "my-token",
          customAuthUrl = "REPLACE_ME_WITH_YOUR_URL"
      )
  )
  ditto.updateTransportConfig { config ->
      config.connect.websocketUrls.add("wss://REPLACE_ME_WITH_YOUR_URL_HOST")
  }
  ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
  ditto.disableSyncWithV3()
  ditto.startSync()

  // Register observer with callback
  val observer = ditto.store.registerObserver(
      query = "SELECT * FROM cars WHERE color = :color",
      arguments = mapOf("color" to "red")
  ) { result ->
      runOnUiThread { updateUI(result.items) }
  }

  // Presence
  val presenceObserver = ditto.presence.observe { graph ->
      graph.remotePeers.forEach { println(it.peerKeyString) }
  }

  // Transport condition
  ditto.callback = object : DittoCallback {
      override fun transportConditionDidChange(condition, subsystem) {
          println("$subsystem: $condition")
      }
  }

  // Error handling
  try {
      ditto.sync.start()
  } catch (e: DittoError) {
      when (e) {
          is DittoError.StoreError -> println(e.reason)
          else -> println(e.message)
      }
  }

  // Cleanup
  observer.close()
  presenceObserver.close()
  ```

  AFTER (V5):

  ```kotlin
  import com.ditto.kotlin.*
  import com.ditto.kotlin.transports.*
  import com.ditto.kotlin.serialization.*

  // Initialize
  val ditto = DittoFactory.create(
      DittoConfig(
          databaseId = "my-app-id",
          connect = DittoConfig.Connect.Server("REPLACE_ME_WITH_YOUR_URL"),
      )
  )

  // Authentication
  ditto.auth?.let { auth ->
      auth.expirationHandler = { dittoInstance, timeUntilExpiration ->
          dittoInstance.auth?.login(
              token = "my-token",
              provider = DittoAuthenticationProvider.development(),
          )
      }
  }

  ditto.sync.start()

  // Register observer — handler is now suspend. Extract data from result.items
  // inside the lambda; never pass result.items out as-is (use-after-free).
  val observer = ditto.store.registerObserver(
      query = "SELECT * FROM cars WHERE color = :color",
      arguments = mapOf("color" to "red")
  ) { result ->
      val cars = result.items.map { carJson.decodeFromString<Car>(it.jsonString()) }
      withContext(Dispatchers.Main) { updateUI(cars) }
  }

  // OR use observe() Flow — cancelled with lifecycleScope.
  // observe()'s `transform` lambda MUST NOT return `DittoQueryResult` or any
  // `DittoQueryResultItem` — they close after it returns. Return only plain
  // Kotlin data (here: List<String>).
  lifecycleScope.launch {
      ditto.store.observe(
          "SELECT * FROM cars WHERE color = :color",
          mapOf("color" to "red")
      ) { result ->
          result.items.map { it.jsonString() }
      }.collect { jsonStrings ->
          updateUI(jsonStrings.map { carJson.decodeFromString<Car>(it) })
      }
  }

  // Presence — Flow-based
  lifecycleScope.launch {
      ditto.presence.observe().collect { graph ->
          graph.remotePeers.forEach { println(it.peerKey) }
      }
  }

  // Transport condition — Flow-based
  lifecycleScope.launch {
      ditto.transportCondition.collect { event ->
          println("${event.subsystem}: ${event.condition}")
      }
  }

  // Error handling
  try {
      ditto.sync.start()
  } catch (e: DittoException) {
      when (e) {
          is DittoException.StoreException -> println(e.reason)
          else -> println(e.message)
      }
  }

  // Cleanup — observer still needs manual close if not using Flow
  observer.close()
  // Flow-based observers auto-cancel with lifecycleScope — no cleanup needed
  ```

  DQL MIGRATION EXAMPLES:

  ```kotlin
  // Query — v4: store.collection("cars").find("color == 'blue'").exec()
  val cars = ditto.store.execute(
      query = "SELECT * FROM cars WHERE color = :color",
      arguments = mapOf("color" to "blue")
  ) { result -> result.items.toList() }

  // Insert — v4: store.collection("cars").upsert(mapOf("_id" to id, "color" to "blue"))
  ditto.store.execute(
      query = "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE",
      arguments = mapOf("doc" to mapOf("_id" to id, "color" to "blue"))
  )

  // Update — v4: store.collection("cars").findById(id).update { doc -> doc["color"].set("green") }
  ditto.store.execute(
      query = "UPDATE cars SET color = :color WHERE _id = :id",
      arguments = mapOf("color" to "green", "id" to id)
  )

  // Delete — v4: store.collection("cars").findById(id).remove()
  ditto.store.execute(
      query = "DELETE FROM cars WHERE _id = :id",
      arguments = mapOf("id" to id)
  )

  // Evict — v4: store.collection("cars").findById(id).evict()
  ditto.store.execute(
      query = "EVICT FROM cars WHERE _id = :id",
      arguments = mapOf("id" to id)
  )

  // Counter — v4: doc["viewCount"].counter.increment(1.0)
  ditto.store.execute(
      query = "UPDATE cars APPLY viewCount PN_INCREMENT BY :n WHERE _id = :id",
      arguments = mapOf("n" to 1, "id" to id)
  )

  // Subscribe — v4: store.collection("cars").find("color == 'red'").subscribe()
  val sub = ditto.sync.registerSubscription(
      query = "SELECT * FROM cars WHERE color = :color",
      arguments = mapOf("color" to "red")
  )
  ```

  CHECKLIST:

  - Replace SDK `live.ditto.*` imports with the corresponding `com.ditto.kotlin.*` packages; add `serialization.*` and `transports.*` imports
  - If DittoAndroidTools is present, update it to `com.ditto:ditto-tools-android:6.0.0` and map `live.ditto.tools.*` imports to `com.ditto.tools.*`
  - Replace `Ditto(dependencies, identity)` / `Ditto.open()` / `Ditto.openSync()` with `DittoFactory.create(DittoConfig(...))`
  - Remove `DefaultAndroidDittoDependencies`, `DittoIdentity`, `DittoAndroidConfig` usage
  - Replace `DittoConfigConnect.Server(URI(...))` with `DittoConfig.Connect.Server("url")`
  - Replace `startSync()` / `stopSync()` with `sync.start()` / `sync.stop()`
  - Set `auth.expirationHandler` suspend lambda for authentication
  - Replace `DittoAuthenticationCallback` interface with expiration handler
  - Replace callback-based `login()` with `suspend auth.login(token, provider)`
  - Replace collection-based queries with DQL via `store.execute()`
  - Update `store.execute()` calls: use handler for reads, no-handler overload for writes, `executeRaw()` for manual lifecycle
  - Convert observer callbacks to suspend handlers, or use `observe()` Flow, or `collect()` for backpressure
  - Replace `runOnUiThread` with `withContext(Dispatchers.Main)` in suspend handlers
  - Update transport config to builder DSL pattern
  - Replace `ditto.callback` with `ditto.transportCondition` Flow
  - Replace `presence.observe(handler)` with `presence.observe()` Flow
  - Replace `DittoError` catches with `DittoException`; update all subclass names (`StoreError` → `StoreException`, etc.)
  - Replace `peerKeyString` with `peerKey`; `isConnectedToDittoCloud` with `isConnectedToDittoServer`
  - Replace callback-based `fetchAttachment()` with suspend version returning `DittoAttachmentFetchResult`
  - Rename `DiskUsage` → `DittoDiskUsage`, `DiskUsageItem` → `DittoDiskUsageItem`; update `diskUsage.exec` → `diskUsage.item`
  - Update `DittoLogger.enabled` → `.isEnabled`; `DittoLogLevel.DEBUG` → `.Debug`
  - Replace `setCustomLogCallback()` with `observeLogEvents()` Flow
  - Remove `DittoTransportConfig.enableAllPeerToPeer()` — configure each transport explicitly
  - Remove `disableSyncWithV3()`, `runGarbageCollection()`, `getTransportDiagnostics()`
  - Set `DQL_STRICT_MODE = true` BEFORE `sync.start()` if your v4 app relied on the v4 default
  - Replace `store.write(block)` with `store.transaction { DittoTransaction.Result.Commit(value) }`
  - Update `DittoAttachment.metadata` / `DittoAttachmentToken.metadata` type from `Map<String, String>` to `Map<String, Any?>` (or `DittoCborSerializable.Dictionary`)

  MANDATORY FINAL STEP — BUILD AND FIX ALL ERRORS:
  After completing ALL code changes above, you MUST attempt to build the project and fix every compilation error before declaring the migration complete.

  Run: `./gradlew assembleDebug` (or build in Android Studio)

  Do NOT stop when the IDE shows no red underlines — actually build the project. Fix every error before finishing.

  Most common build errors and their fixes:

  - `Unresolved reference: live.ditto` → missed a v4 SDK or tools import; map SDK imports to `com.ditto.kotlin.*` and tools imports to `com.ditto.tools.*`
  - `Unresolved reference: DittoSyncPermissions` → it moved to `com.ditto.kotlin.transports`; update import to `import com.ditto.kotlin.transports.DittoSyncPermissions`
  - `Unresolved reference: items` on a `store.execute()` call → you used the no-handler overload (returns Unit); switch to handler form: `store.execute(query, args) { result -> result.items.toList() }`
  - `Type mismatch: inferred type is Unit but X was expected` on store.execute result → same fix as above
  - `Unresolved reference: setExpirationHandler` → use property assignment: `auth.expirationHandler = { ditto, t -> ... }`
  - `Suspension functions can be called only within coroutine body` inside expirationHandler → the lambda IS already suspend; do not wrap in `launch {}`; call `auth.login()` directly
  - `Unresolved reference: DittoError` → replace with `DittoException`
  - `Unresolved reference: startSync` / `stopSync` / `isSyncActive` → replace with `sync.start()` / `sync.stop()` / `sync.isActive`
  - `Unresolved reference: DefaultAndroidDittoDependencies` → remove entirely; no replacement needed
  - Cast `doc.value["field"] as SomeType` throws `ClassCastException` at runtime → use `item.jsonString()` + JSON deserialization, or typed accessors (`.stringOrNull`, `.intOrNull`, etc.)
  - `error: <identifier> expected` in generated BuildConfig.java with double-quoted values like `""value""` → this is a pre-existing project issue where your .env file uses shell-style quoted values (`KEY="value"`) that Java's `Properties.load()` preserves literally; fix by stripping surrounding quotes from property values before passing them to `BuildConfigField`, e.g.: `val rawValue = prop[key]?.toString()?.trim('"') ?: ""`

  Work through the codebase systematically. Show me each file's changes and confirm a successful build at the end.

  ````
</Accordion>

***

## Upgrade DittoAndroidTools

If the application uses DittoAndroidTools, replace the old Maven coordinate with version 6:

<CodeGroup>
  ```kotlin KOTLIN (v5) theme={null}
  implementation("com.ditto:ditto-tools-android:6.0.0")
  ```

  ```kotlin KOTLIN (v4) theme={null}
  implementation("live.ditto:ditto-tools-android:5.0.2")
  ```
</CodeGroup>

Update tools imports separately from SDK imports:

* `live.ditto.tools.*` → `com.ditto.tools.*`
* `live.ditto.*` SDK imports → the corresponding `com.ditto.kotlin.*` packages

DittoAndroidTools 6 requires `com.ditto:ditto-kotlin` 5.0.1 or a later 5.x release and Android API 24 or later.

Code that reads `DittoHeartbeatInfo.peerKeyString` must use `peerKey` instead. The heartbeat document field remains `peerKey`.

See the [DittoAndroidTools releases](https://github.com/getditto/DittoAndroidTools/releases) for the complete release notes.

***

## SDK Package Rename

Ditto SDK imports under `live.ditto.*` have moved to `com.ditto.kotlin.*`. If the application uses DittoAndroidTools, map its `live.ditto.tools.*` imports to `com.ditto.tools.*` instead.

You must replace your definition in your gradle file:

<CodeGroup>
  ```kotlin KOTLIN (v5) theme={null}
  ditto-kotlin = { module = "com.ditto:ditto-kotlin", version.ref = "ditto" }
  ```

  ```kotlin KOTLIN (v4) theme={null}
  live-ditto = { group = "live.ditto", name = "ditto", version.ref = "ditto" }
  ```
</CodeGroup>

* Use the KMP root artifact `com.ditto:ditto-kotlin` (the same coordinate the install guide and the v5 tools use). For an Android app, Gradle reads the module metadata and automatically resolves the platform-specific `-android` variant — you do not need to write `com.ditto:ditto-kotlin-android` yourself.
* `live.ditto.*` → `com.ditto.kotlin.*`
* `live.ditto.transports.*` → `com.ditto.kotlin.transports.*`
* `live.ditto.android.*` → `com.ditto.kotlin.*` (Android-specific classes merged into main package)
* Replace SDK imports by package family instead of blindly replacing every `live.ditto` occurrence.

## Minimum Supported Android Version

Ditto SDK v5 requires `minSdk = 24` (Android 7.0 Nougat). If your project targets `minSdk = 23`, you must raise it.

## Ditto Instance Initialization

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

<CodeGroup>
  ```kotlin KOTLIN (v5) theme={null}
  // 1. Configure
  val config = DittoConfig(
      databaseId = "your-database-id",
      connect = DittoConfig.Connect.Server(
          url = "REPLACE_ME_WITH_YOUR_URL"
      )
  )

  // 2. Initialize
  val ditto = DittoFactory.create(config)

  // 3. Authenticate
  ditto.auth?.let { auth ->
      auth.expirationHandler = { ditto, timeUntilExpiration ->
          ditto.auth?.login(
              token = "your-token",
              provider = DittoAuthenticationProvider.development()
          )
      }
  }

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

  ```kotlin KOTLIN (v4) theme={null}
  // Everything mixed together
  val ditto = Ditto(
      androidDependencies,
      DittoIdentity.OnlinePlayground(
          dependencies = androidDependencies,
          appId = "your-app-id",
          token = "your-token",
          customAuthUrl = "REPLACE_ME_WITH_YOUR_URL"
      )
  )

  // Workarounds
  ditto.updateTransportConfig { config ->
      config.connect.websocketUrls.add(webSocketURL)
  }
  ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
  ditto.disableSyncWithV3()

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

**These changes provide:**

* Clear separation of connection, initialization, and authentication concerns
* No workarounds needed (transport config, DQL strict mode, v3 sync disable)
* Simpler package structure (`com.ditto.kotlin.*` instead of `live.ditto.*`)

**Package change:** Update all imports from `live.ditto.*` to `com.ditto.kotlin.*`

**Initialization Migration Steps**

<Steps>
  <Step title="Replace Initialization">
    Replace `Ditto()` constructor with `DittoFactory.create(config)`.

    <CodeGroup>
      ```kotlin KOTLIN (v5) theme={null}
      import com.ditto.kotlin.*

      val ditto = DittoFactory.create(
        DittoConfig(
          databaseId = "your-database-id",
          connect = DittoConfig.Connect.Server("REPLACE_ME_WITH_YOUR_URL")
        )
      )

      // Set up auth handler before starting sync (see Step 2)
      ditto.sync.start()
      ```

      ```kotlin KOTLIN (v4) theme={null}
      import live.ditto.*

      ditto = Ditto(
          DefaultAndroidDittoDependencies(context),
          DittoIdentity.OnlinePlayground(
              dependencies = androidDependencies,
              appId = "your-app-id",
              token = "your-token",
              customAuthUrl = "REPLACE_ME_WITH_YOUR_URL"
          )
      )
      ditto.startSync()
      ```
    </CodeGroup>

    **Key changes:**

    * Update imports: `live.ditto.*` → `com.ditto.kotlin.*`
    * Use `DittoConfig` instead of `DittoIdentity`
    * `appId` → `databaseId`
    * `DittoIdentity.OnlinePlayground` → `DittoConfig.Connect.Server(url = "...")`
    * `DittoIdentity.OfflinePlayground` → `DittoConfig.Connect.SmallPeersOnly(privateKey = null)`
    * `DittoFactory.create()` is not suspend -- no coroutine needed
    * Remove `updateTransportConfig`, `disableSyncWithV3()`, and DQL strict mode queries
    * Remove `DefaultAndroidDittoDependencies` parameter
  </Step>

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

    <CodeGroup>
      ```kotlin KOTLIN (v5) theme={null}
      // Set handler after DittoFactory.create()
      ditto.auth?.let { auth ->
          auth.expirationHandler = { ditto, timeUntilExpiration ->
              // Initial auth or token refresh
              ditto.auth?.login(
                  token = "your-token",
                  provider = DittoAuthenticationProvider.development()
              )
          }
      }
      ```

      ```kotlin KOTLIN (v4) theme={null}
      // Auth mixed with identity
      val ditto = Ditto(
          androidDependencies,
          DittoIdentity.OnlinePlayground(
              dependencies = androidDependencies,
              appId = "...",
              token = "...",  // Token in identity
              customAuthUrl = "..."
          )
      )
      ```
    </CodeGroup>

    **Key changes:**

    * Authentication now uses `expirationHandler` property (suspend lambda)
    * Handler called when auth required (`timeUntilExpiration == 0.0`) or token expiring
    * Use `DittoAuthenticationProvider.development()` for playground tokens
    * Custom auth: `DittoAuthenticationProvider.custom("your-provider")`
  </Step>
</Steps>

***

## Additional Changes

### Observer Changes

v5 offers three observer patterns. `registerObserver()` and `collect()` invoke a `suspend` handler; `observe()` returns a cold `Flow<T>` whose transform converts each result into plain data before emission.

<Warning>
  **Query-result lifetime — do not return `DittoQueryResult` or its items from a handler/transform.** A `DittoQueryResult` and the `DittoQueryResultItem`s it owns are native resources whose lifetime is bound to the observer's current event. As soon as your handler returns (or the next event arrives), they are closed. Reaching back into `result.items` from outside the lambda — or storing them in a `var`/`StateFlow` — will throw `IllegalStateException: Query result item has already been closed`.

  Always extract the data you need (call `item.jsonString()`, `item.value["..."].string`, etc.) **inside** the lambda and return plain Kotlin objects.
</Warning>

<CodeGroup>
  ```kotlin KOTLIN (v5 — registerObserver with suspend handler) theme={null}
  // registerObserver returns DittoStoreObserver, handler is now suspend.
  // Extract data inside the lambda — do not hand result.items to other coroutines.
  val observer = ditto.store.registerObserver(
      query = "SELECT * FROM cars WHERE color = :color",
      arguments = mapOf("color" to "red")
  ) { result ->
      val cars = result.items.map { carJson.decodeFromString<Car>(it.jsonString()) }
      withContext(Dispatchers.Main) {
          this@MyActivity.cars = cars   // safe: plain data, no FFI handles
      }
  }

  // Still requires manual cleanup
  observer.close()
  ```

  ```kotlin KOTLIN (v5 — observe() Flow, auto-cancelled) theme={null}
  // observe() returns Flow<T>. Its `transform` lambda MUST NOT return
  // `result.items` or any `DittoQueryResultItem` — they close after it returns.
  // Convert to plain Kotlin data; returning Ditto structures directly will
  // use-after-free.
  lifecycleScope.launch {
      ditto.store.observe(
          "SELECT * FROM cars WHERE color = :color",
          mapOf("color" to "red")
      ) { result ->
          result.items.map { carJson.decodeFromString<Car>(it.jsonString()) }
      }.collect { cars ->
          this@MyActivity.cars = cars   // safe: plain data
      }
  }
  // No manual cleanup needed — cancelled with lifecycleScope
  ```

  ```kotlin KOTLIN (v4) theme={null}
  var observer: DittoStoreObserver? = null

  observer = ditto.store.registerObserver(
      query = "SELECT * FROM cars WHERE color = :color",
      arguments = mapOf("color" to "red")
  ) { result ->
      runOnUiThread {
          cars = result.items
      }
  }

  override fun onDestroy() {
      super.onDestroy()
      observer?.close()
  }
  ```
</CodeGroup>

**Key changes:**

* `registerObserver()` handler is now `suspend`; still returns `DittoStoreObserver` (manual `.close()`).
* New `observe()` returns `Flow<T>` with a transform lambda — cancelled with the collecting scope (e.g. `lifecycleScope`).
* New `collect()` is a suspending function — Ditto only signals the next event after your handler returns, giving you back-pressure via coroutine suspension.
* Replace `runOnUiThread` with `withContext(Dispatchers.Main)`.
* **Need diffs?** `registerObserver`, `collect`, and `observe` each have an overload whose lambda takes a second `DittoDiff` parameter — e.g. `registerObserver(query, args) { result, diff -> ... }`. `DittoDiff` exposes `insertions`, `deletions`, and `updates` (each a `Set<Int>` of indices into the result list) plus `moves: List<DittoDiff.Move>`.
* v4's standalone `com.ditto.kotlin.DittoDiffer()` is no longer user-constructible — the class survives with an internal constructor and its lifecycle is now owned by the observer. If you were computing diffs manually via `DittoDiffer().diff(result.items)`, switch to the `(result, diff)` observer overload above; the observer creates and closes the differ for you, which also closes a native-memory leak footgun that existed in v4 when `startObserving` orphaned the previous differ (see [ditto#21533](https://github.com/getditto/ditto/pull/21533)).
* v4's `registerObserver(query, args) { result, signalNext -> }` back-pressure overload is **removed** — there is no `signalNext` parameter in v5. Back-pressure is now automatic via the `suspend` handler (see **Back-pressure** below); migrate any manual `signalNext` logic to the `suspend`/`collect()` model.

#### Back-pressure

`registerObserver()` and `collect()` provide back-pressure automatically: while their `suspend` handler is running or suspended, Ditto coalesces changes and only delivers the next event once your handler returns.

<Note>
  **Coalescing**: changes that occur while your handler is running are merged. When the next callback fires you receive the latest state, not every intermediate change.
</Note>

```kotlin theme={null}
// collect() — back-pressure via coroutine suspension.
lifecycleScope.launch {
    ditto.store.collect(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "blue"),
    ) { result ->
        val cars = result.items.map { carJson.decodeFromString<Car>(it.jsonString()) }
        expensiveOperation(cars)
        withContext(Dispatchers.Main) { updateUI(cars) }
        // No new event is delivered until this lambda returns.
    }
}
```

`observe()` does **not** provide natural back-pressure. The returned Flow signals Ditto to produce the next event right after `emit` succeeds, so a slow collector will not slow down the producer — you may receive emissions faster than your collector can drain them. If you need rendezvous-style back-pressure on the Flow side, add an explicit operator:

```kotlin theme={null}
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.conflate

lifecycleScope.launch {
    ditto.store.observe(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "blue"),
    ) { result ->
        result.items.map { carJson.decodeFromString<Car>(it.jsonString()) }
    }
        // Pick the operator that matches your use case:
        //   .buffer(Channel.RENDEZVOUS) — producer suspends until the collector takes a value
        //   .conflate()                 — keep only the latest emission when the collector is slow
        .buffer(Channel.RENDEZVOUS)
        .collect { cars ->
            updateUI(cars)
        }
}
```

For UI snapshots (you only care about the latest state), `.conflate()` is usually the right choice. For ordered processing where every event matters, prefer `collect()` over `observe()` — the suspend handler model is the only API that natively rate-limits the producer to your handler's pace.

### Serialization

v5 adds inline reified overloads that accept `@Serializable` types:

<CodeGroup>
  ```kotlin theme={null}
  @Serializable
  data class CarArgs(val color: String)

  // No .toDittoCbor() needed -- serialized automatically
  ditto.store.execute(
      "SELECT * FROM cars WHERE color = :color",
      CarArgs(color = "red")
  )
  ```
</CodeGroup>

### 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>
      ```kotlin Kotlin (v5 - Maintain v4 Behavior) highlight={6,7} theme={null}
          val ditto = DittoFactory.create(
              DittoConfig(
                  databaseId = "your-database-id",
                  connect = DittoConfig.Connect.Server("REPLACE_ME_WITH_YOUR_URL"),
              )
          )
          ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = true")
          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.

    **Recommended**: Remove the explicit `ALTER SYSTEM SET DQL_STRICT_MODE = false` statement entirely — the v5 default already matches your intent, so the line is now redundant.

    <CodeGroup>
      ```kotlin Kotlin (v5) theme={null}
      val config = DittoConfig(
          databaseId = "your-database-id",
          connect = DittoConfig.Connect.Server(url = "REPLACE_ME_WITH_YOUR_URL")
      )
      val ditto = DittoFactory.create(config)

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

      ```kotlin Kotlin (v4 - Your Current Setup) theme={null}
      val ditto = Ditto(
          DefaultAndroidDittoDependencies(context),
          DittoIdentity.OnlinePlayground(
              dependencies = androidDependencies,
              appId = appId,
              token = playgroundToken,
              customAuthUrl = authUrl
          )
      )

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

      ditto.startSync()
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

For additional guidance or questions, contact Ditto Customer Support.

### Default Persistence Directory

<Warning>
  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.  You only need to set the persistence directory if you provided a custom directory in v4.
</Warning>

**To maintain v4 compatibility:**

```kotlin theme={null}
val ditto = DittoFactory.create(
    DittoConfig(
        databaseId = "your-database-id",
        connect = DittoConfig.Connect.Server("REPLACE_ME_WITH_YOUR_URL"),
        persistenceDirectory = context.filesDir.resolve("ditto").absolutePath
    )
)
```

### API Renames

| Area      | v4.14                                                            | v5.0                                                                                                                                     |
| --------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Package   | `live.ditto.*`                                                   | `com.ditto.kotlin.*`                                                                                                                     |
| Init      | `Ditto(deps, DittoIdentity.OnlinePlayground(...))`               | `DittoFactory.create(DittoConfig(...))`                                                                                                  |
| Config    | `DittoAndroidConfig(context, databaseId, connect, ...)`          | `DittoConfig(databaseId, connect, ...)` -- no Context param; Android context handled internally via `DittoInitializer`                   |
| Connect   | `DittoConfigConnect.Server(url: URI)`                            | `DittoConfig.Connect.Server(url: String)`                                                                                                |
| Sync      | `ditto.startSync()` / `stopSync()`                               | `ditto.sync.start()` / `sync.stop()`                                                                                                     |
| Auth      | `DittoAuthenticationCallback` interface                          | `auth.expirationHandler = { ditto, timeUntilExpiration -> ... }` suspend lambda                                                          |
| Observers | Callback-based `registerObserver()` returns `DittoStoreObserver` | `registerObserver()` with suspend handler returns `DittoStoreObserver`, or `observe()` returning `Flow`, or `collect()` for backpressure |
| Errors    | `DittoError` sealed class                                        | `DittoException` sealed class                                                                                                            |
| Params    | `Map<String, Any?>` with plain Kotlin values                     | `Map<String, Any?>`, `@Serializable` types, or `DittoCborSerializable.Dictionary`                                                        |
| Transport | Mutable `DittoTransportConfig` data class                        | `DittoTransportConfig` modified via `updateTransportConfig()` builder DSL                                                                |
| Results   | `DittoQueryResultItem.value: Map<String, Any>`                   | `DittoQueryResultItem.value: DittoCborSerializable.Dictionary`                                                                           |
| Closeable | Classes implement `java.io.Closeable`                            | Classes implement `DittoResource` (extends `AutoCloseable`)                                                                              |
| Presence  | `presence.observe(handler)` returns `DittoPresenceObserver`      | `presence.observe()` returns `Flow<DittoPresenceGraph>`                                                                                  |
| DiskUsage | `DiskUsage`, `DiskUsageItem`, `FileSystemType`                   | `DittoDiskUsage`, `DittoDiskUsageItem`, `DittoFileSystemType`                                                                            |
| Peers     | `peerKeyString`, `isConnectedToDittoCloud`                       | `peerKey`, `isConnectedToDittoServer`                                                                                                    |

### Key Initialization Changes

| v4.14                                                   | v5.0                                                     | Notes                                                                          |
| ------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `Ditto(dependencies, identity)` constructor             | **Removed**                                              | Use `DittoFactory.create()`                                                    |
| `Ditto.open(DittoAndroidConfig)` suspend                | **Removed**                                              | Use `DittoFactory.create()` (not suspend)                                      |
| `Ditto.openSync(DittoAndroidConfig)`                    | **Removed**                                              | Use `DittoFactory.create()` (not suspend)                                      |
| `DittoAndroidConfig(context, databaseId, connect, ...)` | **Removed**                                              | Use `DittoConfig(databaseId, connect, ...)` -- no Context parameter            |
| `DittoConfigConnect.Server(url: URI)`                   | `DittoConfig.Connect.Server(url: String)`                | URL is now a `String`, not `java.net.URI`; nested inside `DittoConfig`         |
| `DittoConfigConnect.SmallPeersOnly(privateKey?)`        | `DittoConfig.Connect.SmallPeersOnly(privateKey?)`        | Now nested inside `DittoConfig`                                                |
| `DittoConfigExperimental(coreExceptionBridgingEnabled)` | `DittoConfig.Experimental(coreExceptionBridgingEnabled)` | Now nested inside `DittoConfig`                                                |
| `DittoConfig` properties are `var` (mutable)            | `DittoConfig` properties are `val` (immutable)           | Config is now immutable after construction                                     |
| `DittoConfig.persistenceDirectory: File?`               | `DittoConfig.persistenceDirectory: String?`              | Changed from `File` to `String`                                                |
| `DefaultAndroidDittoDependencies(context)`              | **Removed**                                              | Android context is handled internally by `DittoInitializer` (AndroidX Startup) |
| `AndroidDittoDependencies` interface                    | **Removed**                                              | No dependency injection needed                                                 |
| `DittoDependencies` interface                           | **Removed**                                              | No dependency injection needed                                                 |
| `DefaultDittoDependencies(baseDir)`                     | **Removed**                                              | No dependency injection needed                                                 |
| `DittoBase` abstract class                              | **Removed**                                              | `Ditto` is the single concrete class                                           |

### Identity Type Mapping

| v4.14 DittoIdentity                                    | v5.0 Equivalent                                                                                                               |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `OnlinePlayground(deps, appId, token, ...)`            | `DittoConfig.Connect.Server(url)` + `auth.expirationHandler` + `auth.login(token, DittoAuthenticationProvider.development())` |
| `OnlineWithAuthentication(deps, appId, callback, ...)` | `DittoConfig.Connect.Server(url)` + `auth.expirationHandler` + custom auth logic                                              |
| `OfflinePlayground(deps, appId)`                       | `DittoConfig.Connect.SmallPeersOnly()` + `ditto.setOfflineOnlyLicenseToken(token)`                                            |
| `SharedKey(deps, appId, sharedKey)`                    | `DittoConfig.Connect.SmallPeersOnly(privateKey = sharedKey)`                                                                  |
| `Manual(certificateConfig)`                            | **Removed** -- no direct equivalent                                                                                           |

### Authentication API Changes

| v4.14                                                                          | v5.0                                                         | Notes                                                         |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------- |
| `DittoAuthenticationCallback` interface                                        | **Removed**                                                  | Use `auth.expirationHandler` suspend lambda                   |
| `authenticator.loginWithToken(token, provider, callback)`                      | **Removed** (deprecated in v4)                               | Use `auth.login(token, provider)` suspend                     |
| `authenticator.loginWithCredentials(username, password, provider, callback)`   | **Removed** (deprecated in v4)                               | No direct replacement                                         |
| `auth.login(token, provider, completion: (String?, DittoError?) -> Unit)`      | **Removed**                                                  | Use `auth.login(token, provider)` suspend (returns `String?`) |
| `DittoLoginCallback` interface                                                 | **Removed**                                                  |                                                               |
| `DittoLoginCompletionCallback` interface                                       | **Removed**                                                  |                                                               |
| `DittoAuthenticationStatusDidChangeCallback` interface                         | **Removed**                                                  | Use `auth.observeStatus()` Flow                               |
| `DittoLogoutCleanupFn` interface                                               | **Removed**                                                  | Use lambda directly                                           |
| `auth.observeStatus(callback: (DittoAuthenticationStatus) -> Unit): Closeable` | `auth.observeStatus(): StateFlow<DittoAuthenticationStatus>` | Returns `StateFlow` instead of `Closeable`                    |
| `auth.logout(cleanupFn: ((DittoBase) -> Unit)?)`                               | `auth.logout(cleanupFn: ((Ditto) -> Unit)?)`                 | Parameter type changed from `DittoBase` to `Ditto`            |
| `auth.expirationHandler: suspend (DittoBase, Double) -> Unit`                  | `auth.expirationHandler: suspend (Ditto, Double) -> Unit`    | Parameter type changed from `DittoBase` to `Ditto`            |

### Key Store API Changes

| v4.14                                               | v5.0                                                                                                         | Notes                                                                |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `store.execute(query, arguments): DittoQueryResult` | `store.execute(query, arguments)` (discards result) or `store.execute(query, args) { handler }` (auto-close) | Use `executeRaw()` if you need the old behavior                      |
| `store.executeBlocking(query, arguments)`           | **Removed**                                                                                                  | Use `runBlocking { store.execute(...) }` if needed                   |
| `store.collection(name)`                            | **Removed**                                                                                                  | Use DQL queries with `store.execute()`                               |
| `store[name]` operator                              | **Removed**                                                                                                  | Use DQL queries                                                      |
| `store.collectionNames()`                           | **Removed**                                                                                                  | Use `store.execute("SELECT DISTINCT collection FROM __collections")` |
| `store.collections()`                               | **Removed**                                                                                                  | Use DQL queries                                                      |
| `store.write(block)`                                | **Removed**                                                                                                  | Use `store.transaction()`                                            |
| `store.queriesHash()` / `queriesHashMnemonic()`     | **Removed**                                                                                                  |                                                                      |

### Observer Migration Guide

| v4.14 Pattern                                              | v5.0 Replacement                                                                 |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `registerObserver(query, args) { result -> ... }` callback | `registerObserver(query, args) { result -> ... }` (handler is now `suspend`)     |
| `registerObserver` + `DittoSignalNext` for backpressure    | `collect()` -- backpressure via coroutine suspension                             |
| Manual `observer.close()` in `onDestroy()`                 | Same pattern, or use `observe().collect {}` in `lifecycleScope` (auto-cancelled) |
| `runOnUiThread { ... }` inside handler                     | `withContext(Dispatchers.Main) { ... }` inside suspend handler                   |

### Removed Observer Types

| v4.14                                         | v5.0        | Notes                                                |
| --------------------------------------------- | ----------- | ---------------------------------------------------- |
| `DittoChangeHandler` typealias                | **Removed** | Handler is now `suspend (DittoQueryResult) -> Unit`  |
| `DittoChangeHandlerWithNextSignal` typealias  | **Removed** | Use `collect()` for backpressure                     |
| `DittoSignalNext` / `DittoSignalNextCallback` | **Removed** | Backpressure via coroutine suspension in `collect()` |

### Transaction Changes

| v4.14                                                            | v5.0                                                                          | Notes                                                  |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ |
| `transaction { txn -> DittoTransactionCompletionAction.Commit }` | `transaction { txn -> DittoTransaction.Result.Commit(value) }`                | Now generic, returns value                             |
| `DittoTransactionCompletionAction.Abort`                         | `DittoTransaction.Result.Rollback`                                            | Renamed                                                |
| `transactionBlocking()`                                          | **Removed**                                                                   | Use `runBlocking { store.transaction {} }`             |
| `transactionReturningVoid()`                                     | **Removed**                                                                   | Use `transaction { DittoTransaction.Result.Commit() }` |
| `txn.execute(query, Map<String, Any?>?)`                         | `txn.execute(query, DittoCborSerializable.Dictionary)` or `Map<String, Any?>` | Same overloads as `store.execute()`                    |
| --                                                               | `txn.executeRaw()` (NEW)                                                      | Returns raw `DittoQueryResult` for manual lifecycle    |

### Transport Config Changes

| v4.14                                                               | v5.0                                                              | Notes                                                             |
| ------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| `DittoTransportConfig` mutable data class                           | `DittoTransportConfig` immutable class with `Builder`             |                                                                   |
| `DittoPeerToPeer` data class                                        | `DittoTransportConfig.PeerToPeer`                                 | Nested class                                                      |
| `DittoBluetoothLeConfig` data class                                 | `DittoTransportConfig.PeerToPeer.BluetoothLE`                     | Nested class                                                      |
| `DittoBluetoothLeConfig.maxOutgoing`                                | **Removed**                                                       |                                                                   |
| `DittoBluetoothLeConfig.mtuRequest`                                 | **Removed**                                                       |                                                                   |
| `DittoLanConfig` data class                                         | `DittoTransportConfig.PeerToPeer.Lan`                             | Nested class                                                      |
| `DittoWifiAwareConfig` data class                                   | `DittoTransportConfig.PeerToPeer.WifiAware`                       | Nested class                                                      |
| `DittoConnect` data class                                           | `DittoTransportConfig.Connect`                                    | Nested class                                                      |
| `DittoListen` data class                                            | `DittoTransportConfig.Listen`                                     | Nested class                                                      |
| `DittoTcpListenConfig`                                              | `DittoTransportConfig.Listen.TcpConfig`                           | Nested class                                                      |
| `DittoHttpListenConfig`                                             | `DittoTransportConfig.Listen.HttpConfig`                          | Nested class                                                      |
| `DittoHttpListenConfig.staticContentPath`                           | **Removed**                                                       |                                                                   |
| `DittoGlobalConfig` data class                                      | `DittoTransportConfig.Global`                                     | Nested class                                                      |
| `DittoGlobalConfig.syncGroupLong` / `routingHintLong` (Java compat) | **Removed**                                                       | Use `UInt` directly                                               |
| `enableAllPeerToPeer()`                                             | **Removed**                                                       | Configure each transport explicitly                               |
| `DittoAdvertisementPower` enum: `VERY_LOW`, `LOW`, `MEDIUM`, `HIGH` | `VeryLow`, `Low`, `Medium`, `High`                                | PascalCase enum values                                            |
| `DittoAdvertisementFrequency` enum: `LOW`, `MEDIUM`, `HIGH`         | `Low`, `Medium`, `High`                                           | PascalCase enum values                                            |
| `ditto.transportConfig` get/set                                     | `ditto.transportConfig` get/set; prefer `updateTransportConfig()` | `updateTransportConfig()` builder DSL is the recommended approach |

### Presence API Changes

| v4.14                                                                           | v5.0                                                                                         | Notes                                                  |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `presence.observe(handler)` returns `DittoPresenceObserver`                     | `presence.observe()` returns `Flow<DittoPresenceGraph>`                                      | Flow-based; auto-cancelled with scope                  |
| `DittoPresenceObserver` interface                                               | **Removed**                                                                                  | Use Flow cancellation                                  |
| `DittoPresenceObserverCallback` interface                                       | **Removed**                                                                                  | Use Flow                                               |
| `DittoPresenceGraph.json()`                                                     | `DittoPresenceGraph.serializeToJson()`                                                       | Method renamed                                         |
| `presence.peerMetadata: Map<String, Any?>` (get/set)                            | `presence.peerMetadata: DittoJsonSerializable.ObjectValue` (get/set)                         | Type changed; use `getPeerMetadataMap()` for plain Map |
| --                                                                              | `presence.getPeerMetadataMap(): Map<String, Any?>` (NEW)                                     | Convenience method                                     |
| --                                                                              | `presence.setPeerMetadata(Map<String, Any?>)` (NEW)                                          | Convenience method                                     |
| --                                                                              | `presence.peerMetadataJsonBytes: ByteArray` (NEW)                                            | Binary JSON representation                             |
| `presence.connectionRequestHandler: DittoConnectionRequestHandler?` (typealias) | `presence.connectionRequestHandler: DittoPresence.ConnectionRequestHandler?` (fun interface) |                                                        |
| `presence.connectionRequestHandlerJava: DittoConnectionRequestHandlerCallback?` | **Removed**                                                                                  | Use the fun interface directly                         |
| `DittoConnectionRequest.peerKeyString`                                          | `DittoConnectionRequest.peerKey`                                                             | Property renamed                                       |
| `DittoConnectionRequest.peerMetadata: Map<String, Any?>`                        | `DittoConnectionRequest.peerMetadata: DittoJsonSerializable.ObjectValue`                     | Type changed                                           |
| `DittoConnectionRequest.identityServiceMetadata: Map<String, Any?>`             | `DittoConnectionRequest.identityServiceMetadata: DittoJsonSerializable.ObjectValue`          | Type changed                                           |
| `Presence.peerMetadataMaxSizeInBytes` companion const                           | **Removed** from public API                                                                  |                                                        |

### DittoPeer Changes

| v4.14                                        | v5.0                                                         | Notes                                                                         |
| -------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `peerKeyString: String`                      | `peerKey: String`                                            | Renamed                                                                       |
| `peerKey: DittoPeerKey` (ByteArray)          | **Removed**                                                  | Use `peerKey: String`                                                         |
| `DittoPeerKey` typealias (ByteArray)         | **Removed**                                                  |                                                                               |
| `DittoPeerKey.toKeyString()` extension       | **Removed**                                                  |                                                                               |
| `String.decodePeerKey()` extension           | **Removed**                                                  |                                                                               |
| `isConnectedToDittoCloud: Boolean`           | `isConnectedToDittoServer: Boolean`                          | Renamed                                                                       |
| `os: String?`                                | `os: DittoPeerOs?`                                           | Now an enum: `Generic`, `Ios`, `Tvos`, `Android`, `Linux`, `Windows`, `MacOS` |
| `peerMetadata: Map<String, Any?>`            | `peerMetadata: DittoJsonSerializable.ObjectValue`            | Type changed                                                                  |
| `identityServiceMetadata: Map<String, Any?>` | `identityServiceMetadata: DittoJsonSerializable.ObjectValue` | Type changed                                                                  |
| `queryOverlapGroup: Int` (deprecated)        | **Removed**                                                  |                                                                               |

### DittoConnection Changes

| v4.14                                                         | v5.0            | Notes                         |
| ------------------------------------------------------------- | --------------- | ----------------------------- |
| `peer1: DittoAddress` (deprecated) / `peerKeyString1: String` | `peer1: String` | Simplified to peer key string |
| `peer2: DittoAddress` (deprecated) / `peerKeyString2: String` | `peer2: String` | Simplified to peer key string |
| `approximateDistanceInMeters: Double?`                        | **Removed**     |                               |

### DittoAddress Changes

| v4.14                                     | v5.0                        | Notes                                                              |
| ----------------------------------------- | --------------------------- | ------------------------------------------------------------------ |
| `DittoAddress(siteId, pubkey)` data class | `DittoAddress` opaque class | No public properties beyond `toString()`, `equals()`, `hashCode()` |
| `DittoAddress.siteId` (deprecated)        | **Removed**                 |                                                                    |

### Attachment API Changes

| v4.14                                                                     | v5.0                                                                                          | Notes                                                                  |
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `store.newAttachment(path, Map<String, String>)`                          | `store.newAttachment(path, Map<String, Any?>)`                                                | Metadata type widened; also accepts `DittoCborSerializable.Dictionary` |
| `store.newAttachmentBlocking(inputStream, Map<String, String>)`           | `store.newAttachmentBlocking(inputStream, DittoCborSerializable.Dictionary)`                  | Only CBOR overload for blocking variant                                |
| `store.fetchAttachment(token, callback)` returns `DittoAttachmentFetcher` | `suspend store.fetchAttachment(token, onFetchProgress?)` returns `DittoAttachmentFetchResult` | Now suspend; returns result                                            |
| `DittoAttachmentFetchEvent` sealed class (Progress/Completed/Deleted)     | `DittoAttachmentFetchResult` sealed class (Completed/Deleted)                                 | Progress handled via `onFetchProgress` callback                        |
| `DittoAttachmentFetchEvent.Progress(downloadedBytes, totalBytes)`         | `onFetchProgress: suspend (ULong, ULong) -> Unit` parameter                                   | Separate progress callback                                             |
| `DittoAttachmentFetchEventType` enum                                      | **Removed**                                                                                   | Not needed with sealed result class                                    |
| `DittoAttachmentFetcher` (manual lifecycle)                               | Still exists but `store.fetchAttachment()` is preferred                                       | Tracked in `store.attachmentFetchers` (NEW)                            |
| `DittoCollection.newAttachment()` (deprecated)                            | **Removed**                                                                                   | Use `store.newAttachment()`                                            |
| `DittoCollection.fetchAttachment()` (deprecated)                          | **Removed**                                                                                   | Use `store.fetchAttachment()`                                          |

<Note>
  **Reading attachment bytes**: byte access on a `DittoAttachment` is exposed as functions, not Kotlin properties. Use `attachment.getInputStream().readBytes()` or `attachment.getData()` (returns `ByteArray`). `attachment.data` and `attachment.inputStream` do not resolve as properties and will not compile.
</Note>

### Transport Condition Changes

### Transport Condition (Replaces ditto.callback)

<CodeGroup>
  ```kotlin KOTLIN (v5) theme={null}
  // v5
  lifecycleScope.launch {
      ditto.transportCondition.collect { event ->
          // event.condition, event.subsystem
      }
  }
  ```

  ```kotlin KOTLIN (v4) theme={null}
  // v4
  ditto.callback = object : DittoCallback {
      override fun transportConditionDidChange(
          transportCondition: TransportCondition,
          conditionSource: ConditionSource
      ) { /* handle */ }
  }
  ```
</CodeGroup>

| v4.14                                                              | v5.0                                                                             | Notes                  |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ---------------------- |
| `ditto.callback: DittoCallback?`                                   | `ditto.transportCondition: Flow<DittoTransportConditionEvent>`                   | Flow-based             |
| `DittoCallback` interface                                          | **Removed**                                                                      |                        |
| `DittoTransportConditionChangedCallback` interface                 | **Removed**                                                                      |                        |
| `DittoTransportCondition` + `DittoConditionSource` separate params | `DittoTransportConditionEvent(condition, subsystem)`                             | Wrapped in event class |
| --                                                                 | `DittoTransportCondition.Source` nested enum: `Bluetooth`, `Tcp`, `Awdl`, `Mdns` | Subsystem enum         |

### Logging Changes

| v4.14                                                        | v5.0                                                         | Notes                                                                                                           |
| ------------------------------------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `DittoLogger.enabled: Boolean`                               | `DittoLogger.isEnabled: Boolean`                             | Renamed                                                                                                         |
| `DittoLogger.emojiLogLevelHeadingsEnabled`                   | **Removed**                                                  |                                                                                                                 |
| `DittoLogger.setCustomLogCallback(callback)`                 | **Removed**                                                  | Use `observeLogEvents()`                                                                                        |
| `DittoLogger.unsetCustomLogCallback()`                       | **Removed**                                                  | Cancel the Flow scope                                                                                           |
| `DittoLogger.setCustomLogCallback(DittoLogCallback)`         | **Removed**                                                  |                                                                                                                 |
| `DittoLogCallback` interface                                 | **Removed**                                                  |                                                                                                                 |
| --                                                           | `DittoLogger.observeLogEvents(): Flow<DittoLogEvent>` (NEW)  | Returns Flow of log events                                                                                      |
| --                                                           | `DittoLogger.DittoLogEvent(level, message)` (NEW)            | Log event data class                                                                                            |
| `DittoLogger.exportToFileBlocking(): BigInteger`             | `DittoLogger.exportToFileBlocking(path: String): ULong`      | Return type changed; now requires `path` parameter. Also available: `suspend exportToFile(path: String): ULong` |
| `DittoLogLevel.ERROR`, `WARNING`, `INFO`, `DEBUG`, `VERBOSE` | `DittoLogLevel.Error`, `Warning`, `Info`, `Debug`, `Verbose` | PascalCase enum values                                                                                          |
| `DittoLog.e/w/i/d/v()` methods                               | Same signature in v5                                         | No change                                                                                                       |
| `DittoLogDecorator` interface                                | Same in v5                                                   | No change                                                                                                       |

### Disk Usage Changes

| v4.14                                                 | v5.0                                                       | Notes                       |
| ----------------------------------------------------- | ---------------------------------------------------------- | --------------------------- |
| `DiskUsage` class                                     | `DittoDiskUsage` class                                     | Renamed with `Ditto` prefix |
| `DiskUsageItem` data class                            | `DittoDiskUsageItem` class                                 | Renamed                     |
| `FileSystemType` enum: `DIRECTORY`, `FILE`, `SYMLINK` | `DittoFileSystemType` enum: `Directory`, `File`, `Symlink` | Renamed; PascalCase values  |
| `diskUsage.exec: DiskUsageItem`                       | `diskUsage.item: DittoDiskUsageItem`                       | Property renamed            |
| `DiskUsageItem.sizeInBytes: Int`                      | `DittoDiskUsageItem.sizeInBytes: Long`                     | Widened from Int to Long    |
| `diskUsage.observe(handler): Closeable`               | `diskUsage.observe(): Flow<DittoDiskUsageItem>`            | Flow-based                  |
| `DiskUsageCallback` interface                         | **Removed**                                                | Use Flow                    |

### Error Handling

<CodeGroup>
  ```kotlin KOTLIN (v5) theme={null}
  // v5 — DittoError renamed to DittoException
  try {
      ditto.sync.start()
  } catch (e: DittoException) {
      println("Error: ${e.message}")
  }
  ```

  ```kotlin KOTLIN (v4) theme={null}
  try {
      ditto.sync.start()
  } catch (e: DittoError) {
      println("Error: ${e.message}")
  }
  ```
</CodeGroup>

### Exception Hierarchy Changes

| v4.14 `DittoError`                                  | v5.0 `DittoException`                  | Notes                                                                    |
| --------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------ |
| `ActivationError(reason)`                           | `ActivationException(reason)`          | Renamed; reasons: same naming                                            |
| `AuthenticationError(reason)`                       | `AuthenticationException(reason)`      | Renamed                                                                  |
| `FatalError(message)`                               | `FatalException`                       | Renamed                                                                  |
| `FilesystemError(message)` (deprecated)             | **Removed**                            | Was deprecated in v4                                                     |
| `InternalError(reason)` (deprecated)                | **Removed**                            | Was deprecated in v4                                                     |
| `IoError(reason)`                                   | `IoException(reason)`                  | Renamed                                                                  |
| `LockedWorkingDirectoryError(message)` (deprecated) | **Removed**                            | Was deprecated in v4; use `StoreException(PersistenceDirectoryLocked)`   |
| `PresenceError(reason)`                             | `PresenceException(reason)`            | Renamed                                                                  |
| `StoreError(reason)`                                | `StoreException(reason)`               | Renamed                                                                  |
| `TransportError(reason)`                            | `TransportException(reason)`           | Renamed                                                                  |
| `UnknownError(message)`                             | `UnknownException`                     | Renamed                                                                  |
| `UnsupportedError(message)`                         | `UnsupportedException`                 | Renamed                                                                  |
| `ValidationError(reason)`                           | `ValidationException(reason)`          | Renamed                                                                  |
| --                                                  | `SmallPeerInfoException(reason)` (NEW) | For small peer info errors                                               |
| --                                                  | `DataStreamsException` (NEW)           | For Data Streams API errors                                              |
| --                                                  | `DittoPanicException` (NEW)            | For native crashes; has `nativeCrashMessage` and `nativeCrashStackTrace` |

### Reason Classes Renamed

All `*ErrorReason` sealed classes are renamed to `*ExceptionReason`:

* `ActivationErrorReason` -> `ActivationExceptionReason`
* `AuthenticationErrorReason` -> `AuthenticationExceptionReason`
* `IoErrorReason` -> `IoExceptionReason`
* `PresenceErrorReason` -> `PresenceExceptionReason`
* `StoreErrorReason` -> `StoreExceptionReason`
* `TransportErrorReason` -> `TransportExceptionReason`
* `ValidationErrorReason` -> `ValidationExceptionReason`
* **NEW**: `SmallPeerInfoExceptionReason`

### Specific Reason Changes

| v4.14                                                     | v5.0                                                              | Notes                          |
| --------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------ |
| `PresenceErrorReason.JsonParsingError(message)`           | `PresenceExceptionReason.FailedToEncodeJson(cause)`               | Renamed and param type changed |
| `TransportErrorReason.FailedToDecodeTransportDiagnostics` | **Removed**                                                       |                                |
| `StoreErrorReason.FailedToEncodeValue`                    | `StoreExceptionReason.FailedToEncodeValue(value, message, cause)` | Additional params              |
| --                                                        | `StoreExceptionReason.AttachmentDataRetrievalError` (NEW)         |                                |
| --                                                        | `StoreExceptionReason.AttachmentFileCopyError` (NEW)              |                                |
| --                                                        | `StoreExceptionReason.CrdtError` (NEW)                            |                                |
| --                                                        | `StoreExceptionReason.DocumentContentEncodingFailed` (NEW)        |                                |
| --                                                        | `StoreExceptionReason.FailedToDecodeData(cause, data)` (NEW)      |                                |
| --                                                        | `StoreExceptionReason.FailedToGetDocumentData(path)` (NEW)        |                                |
| --                                                        | `StoreExceptionReason.FailedToGetDocumentIDData(path)` (NEW)      |                                |
| --                                                        | `StoreExceptionReason.InvalidDocumentStructure(cbor)` (NEW)       |                                |
| --                                                        | `StoreExceptionReason.NonStringKeyInDocument(key)` (NEW)          |                                |
| --                                                        | `SmallPeerInfoExceptionReason.SizeLimitExceeded` (NEW)            |                                |
| --                                                        | `SmallPeerInfoExceptionReason.DepthLimitExceeded` (NEW)           |                                |
| --                                                        | `SmallPeerInfoExceptionReason.NotADictionary` (NEW)               |                                |
| `ValidationErrorReason.UnknownSyncScope`                  | **Removed**                                                       |                                |

Update ALL `catch (e: DittoError)` blocks to `catch (e: DittoException)`.

***

## Removed APIs

These v4 classes, interfaces, and APIs have been **completely removed** in v5

### Classes Removed

* `DittoCollection` -- use DQL queries via `store.execute()`
* `DittoDocument` / `DittoMutableDocument` -- results are now `DittoQueryResultItem`; use `item.jsonString()` or typed accessors on `item.value`
* `DittoDocumentId` -- use `_id` field in DQL queries
* `DittoDocumentPath` / `DittoDocumentIdPath` -- access values via typed accessors or `jsonString()`
* `DittoMutableDocumentPath` -- use `UPDATE SET` DQL
* `DittoMutableCounter` / `DittoMutableRegister` -- use DQL `PN_INCREMENT` / `SET`
* `DittoCounter` / `DittoRegister` (property accessors on document paths)
* `DittoPendingCursorOperation` -- use DQL queries
* `DittoPendingIdSpecificOperation` -- use DQL queries
* `DittoPendingCollectionsOperation` -- use DQL queries
* `DittoLiveQuery` -- use `registerObserver()`, `observe()`, or `collect()`
* `DittoLiveQueryEvent` sealed class (`Initial`, `Update`) -- use `DittoDiff`
* `DittoSingleDocumentLiveQueryEvent` -- use `DittoDiff`
* `DittoLiveQueryMove` -- use `DittoDiff.Move`
* `DittoSubscription` (legacy) -- use `DittoSyncSubscription` via `sync.registerSubscription()`
* `DittoWriteTransaction` / `DittoScopedWriteTransaction` -- use `store.transaction()`
* `DittoWriteTransactionPendingCursorOperation` / `DittoWriteTransactionPendingIdSpecificOperation`
* `DittoWriteTransactionResult` sealed class (`Inserted`, `Updated`, `Evicted`, `Removed`) -- use `DittoQueryResult.mutatedDocumentIds()`
* `DittoUpdateResult` sealed class (`Set`, `Removed`, `Incremented`)
* `DittoCollectionsEvent`
* `DittoRemotePeer`
* `DittoTransportDiagnostics`

### Enums Removed

* `DittoWriteStrategy` (`Merge`, `InsertIfAbsent`, `InsertDefaultIfAbsent`, `UpdateDifferentValues`) -- use DQL `ON ID CONFLICT` clauses
* `DittoSortDirection` (`Ascending`, `Descending`) -- use DQL `ORDER BY ... ASC/DESC`
* `DittoConnectionPriority` (`DontConnect`, `Normal`, `High`)
* `DittoSmallPeerInfoSyncScope` (`BigPeerOnly`, `LocalPeerOnly`)
* `DittoBase64PaddingMode`
* `DittoAttachmentFetchEventType` (`Completed`, `Progress`, `Deleted`)

### Interfaces Removed

* `DittoCallback` / `DittoTransportConditionChangedCallback` -- use `transportCondition` Flow
* `DittoAuthenticationCallback` -- use `auth.expirationHandler` suspend lambda
* `DittoLoginCallback` / `DittoLoginCompletionCallback`
* `DittoAuthenticationStatusDidChangeCallback` -- use `auth.observeStatus()` StateFlow
* `DittoLogoutCleanupFn` -- use lambda
* `DittoConnectionRequestHandlerCallback` -- use `DittoPresence.ConnectionRequestHandler` fun interface
* `DittoPeersObserver` / `DittoPeersObserverV1Callback` / `DittoPeersObserverV2Callback`
* `DittoPresenceObserver` -- use Flow
* `DittoPresenceObserverCallback`
* `DittoLiveQueryCallback` / `DittoLiveQueryWithNextSignalCallback`
* `DittoSingleDocumentLiveQueryCallback` / `DittoSingleDocumentLiveQueryWithNextSignalCallback`
* `DittoMutableDocumentsUpdater` / `DittoSingleMutableDocumentUpdater`
* `DittoWriteTransactionHandler`
* `DiskUsageCallback`
* `DittoSignalNextCallback`
* `DittoLogCallback`
* `DittoDependencies` / `DefaultDittoDependencies`
* `AndroidDittoDependencies` / `DefaultAndroidDittoDependencies`
* `JavaDittoDependencies` / `DefaultJavaDittoDependencies`

### Type Aliases Removed

* `DittoChangeHandler`
* `DittoChangeHandlerWithNextSignal`
* `DittoSignalNext`
* `DittoConnectionRequestHandler` (typealias; replaced by `DittoPresence.ConnectionRequestHandler` fun interface)
* `DittoAuthenticationExpirationHandler` / `DittoAuthenticationExpirationHandlerSync` (the handler is still used but type is inline)
* `DittoPeerKey`

***

## New APIs in v5

* **`DittoFactory.create(DittoConfig(...))`** — new initialization via config object (no Context parameter; Android context handled internally via `DittoInitializer`)
* **`DittoConfig.Connect.Server`** / **`DittoConfig.Connect.SmallPeersOnly`** — connect mode types nested inside `DittoConfig`
* **`DittoException`** — replaces `DittoError` as the sealed exception hierarchy
* **`ditto.sync`** sub-object with `start()`, `stop()`, `isActive`, `registerSubscription()`
* **`ditto.transportCondition: Flow<DittoTransportConditionEvent>`** — replaces `ditto.callback`
* **`auth.observeStatus(): StateFlow`** — observe authentication status as a Flow
* **`store.registerObserver()`** — now accepts a suspend handler; returns `DittoStoreObserver`
* **`store.observe()`** — new Flow-based observer returning `Flow<T>` with a transform lambda
* **`store.collect()`** — new suspend function providing natural backpressure via coroutine suspension
* **`presence.observe()`** — now returns `Flow<DittoPresenceGraph>` (was callback-based)
* **Data Streams API (preview)** — streaming data subscription model
* **`updateTransportConfig()`** — preferred builder DSL for modifying transport config

***

## Migration Checklist

### Initialization

* [ ] Update imports: `live.ditto.*` → `com.ditto.kotlin.*`
* [ ] Replace `Ditto(deps, DittoIdentity.OnlinePlayground(...))` with `DittoFactory.create(DittoConfig(...))`
* [ ] Create `DittoConfig` with `databaseId` and `connect` mode
* [ ] Update `DittoIdentity.OnlinePlayground` → `DittoConfig.Connect.Server(url = "...")`
* [ ] Update `DittoIdentity.OfflinePlayground` → `DittoConfig.Connect.SmallPeersOnly(privateKey = null)`
* [ ] Remove `DefaultAndroidDittoDependencies` parameter
* [ ] Remove `updateTransportConfig` calls
* [ ] Remove `disableSyncWithV3()` calls
* [ ] **Set `DQL_STRICT_MODE=true` BEFORE starting sync if maintaining v4 behavior**
* [ ] Update `startSync()` → `sync.start()`

### Observers

* [ ] Convert callback observers to suspend handlers or Flow-based `observe()`/`collect()`
* [ ] Wrap observers in `lifecycleScope.launch { }`
* [ ] Replace `runOnUiThread` with `withContext(Dispatchers.Main)`
* [ ] Remove manual observer `.close()` calls when using Flow-based observers
* [ ] Remove observer property tracking

### Authentication

* [ ] Set `expirationHandler` property on `auth` after initialization
* [ ] Use `DittoAuthenticationProvider.development()` for playground tokens
* [ ] Use `DittoAuthenticationProvider.custom("provider")` for custom auth providers
* [ ] Remove authentication from identity configuration

### Query Arguments

* [ ] Use `Map<String, Any?>` for DQL query arguments (recommended)
* [ ] Alternatively, use `@Serializable` data classes for type-safe arguments
* [ ] Apply to INSERT, UPDATE, SELECT arguments

### Breaking Changes

* [ ] Set `persistenceDirectory` if maintaining v4 directory structure
* [ ] Update `peerKeyString` → `peerKey`
* [ ] Update ALL `catch (e: DittoError)` to `catch (e: DittoException)`
* [ ] Replace `ditto.callback` with `ditto.transportCondition: Flow<...>`
* [ ] Update `isConnectedToDittoCloud` → `isConnectedToDittoServer`

### Verification

* [ ] Build compiles with zero errors
* [ ] No deprecated API warnings
* [ ] Observers update UI immediately
* [ ] Authentication works before sync starts
* [ ] No `ClassCastException` in serialization
* [ ] No memory leaks on Fragment navigation

***

```
```


## Related topics

- [Flutter V4→V5 API Migration Guide](/sdk/latest/migration-guides/flutter-v4.md)
- [C++ V4→V5 API Migration Guide](/sdk/latest/migration-guides/cpp-v4.md)
- [C# V4→V5 API Migration Guide](/sdk/latest/migration-guides/c-sharp-v4.md)
