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

  PACKAGE RENAME:
  Replace all `live.ditto.*` imports with `com.ditto.kotlin.*`.

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

  ```kotlin
  //v5
  ditto-kotlin = { module = "com.ditto:ditto-kotlin-android", 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
  - Use IDE Find & Replace: `live.ditto` → `com.ditto.kotlin` project-wide

  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`
  - `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(
      "SELECT * FROM tasks WHERE _id = :id",
      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), fire-and-forget is correct — no handler needed:
  ```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`
  - Or use `item.jsonString()` to get a JSON string and deserialize from JSON (recommended for complex objects)

  OBSERVERS:
  Three patterns replace the v4 callback model:

  1. `registerObserver(query, args) { result -> ... }` — handler is now `suspend`; returns `DittoStoreObserver`; supports optional `DittoDiff` second parameter
  2. `observe(query, args) { result -> transform }` — NEW; returns `Flow<T>`; auto-cancelled with scope; supports optional `DittoDiff`
  3. `collect(query, args) { result -> ... }` — NEW; suspend function providing natural backpressure via coroutine suspension

  - Replace `runOnUiThread { ... }` with `withContext(Dispatchers.Main) { ... }` inside suspend handlers
  - Replace `DittoSignalNext` / `signalNext` backpressure with `collect()` method
  - Remove `DittoChangeHandler`, `DittoChangeHandlerWithNextSignal`, `DittoSignalNext`, `DittoSignalNextCallback` — all removed
  - When using `observe()` in `lifecycleScope`, observer is auto-cancelled — 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

  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)` — metadata accepts `Map<String, Any?>` or `DittoCborSerializable.Dictionary` (changed from `Map<String, String>`)
  - 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`

  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 = "https://my-app-id.cloud.ditto.live"
      )
  )
  ditto.updateTransportConfig { config ->
      config.connect.websocketUrls.add("wss://my-app-id.cloud.ditto.live")
  }
  ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
  ditto.disableSyncWithV3()
  ditto.startSync()

  // Register observer with callback
  val observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      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("https://my-app-id.cloud.ditto.live"),
      )
  )

  // 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
  val observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      mapOf("color" to "red")
  ) { result ->
      withContext(Dispatchers.Main) { updateUI(result.items) }
  }

  // OR use observe() Flow — auto-cancelled with lifecycleScope
  lifecycleScope.launch {
      ditto.store.observe(
          "SELECT * FROM cars WHERE color = :color",
          mapOf("color" to "red")
      ) { result ->
          result.items.map { it.jsonString() }
      }.collect { cars ->
          updateUI(cars)
      }
  }

  // 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(
      "SELECT * FROM cars WHERE color = :color",
      mapOf("color" to "blue")
  ) { result -> result.items.toList() }

  // Insert — v4: store.collection("cars").upsert(mapOf("_id" to id, "color" to "blue"))
  ditto.store.execute(
      "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE",
      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(
      "UPDATE cars SET color = :color WHERE _id = :id",
      mapOf("color" to "green", "id" to id)
  )

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

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

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

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

  CHECKLIST:
  - Replace ALL `live.ditto.*` imports with `com.ditto.kotlin.*`; add `serialization.*` and `transports.*` imports
  - 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, fire-and-forget 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 an import or usage of a v4 class; replace with `com.ditto.kotlin`
  - `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 fire-and-forget (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>

***

## Package Rename

All imports from `live.ditto.*` have changed to `com.ditto.kotlin.*`

You must replace your definition in your gradle file:

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

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

* `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)
* Use IDE Find & Replace: `live.ditto` → `com.ditto.kotlin` project-wide

## 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 = "https://your-server.ditto.live"
      )
  )

  // 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 = "https://your-server.ditto.live"
      )
  )

  // 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("https://your-server.ditto.live")
        )
      )

      // 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 = "https://your-server.ditto.live"
          )
      )
      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. The `registerObserver()` handler is now a `suspend` lambda. The new `observe()` returns a `Flow<T>`, and `collect()` provides natural backpressure.

<CodeGroup>
  ```kotlin KOTLIN (v5 — registerObserver with suspend handler) theme={null}
  // registerObserver returns DittoStoreObserver, handler is now suspend
  val observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      mapOf("color" to "red")
  ) { result ->
      withContext(Dispatchers.Main) {
          cars = result.items
      }
  }

  // Still requires manual cleanup if not using Flow
  observer.close()
  ```

  ```kotlin KOTLIN (v5 — observe() Flow, auto-cancelled) theme={null}
  // observe() returns Flow<T> with a required transform lambda
  lifecycleScope.launch {
      ditto.store.observe(
          "SELECT * FROM cars WHERE color = :color",
          mapOf("color" to "red")
      ) { result ->
          result.items  // transform result — do not return DittoQueryResult itself
      }.collect { items ->
          cars = items
      }
  }
  // No manual cleanup needed — auto-cancelled with lifecycleScope
  ```

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

  observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      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` (requires manual `.close()`)
* New `observe()` returns `Flow<T>` with a transform lambda — auto-cancelled with `lifecycleScope`
* New `collect()` is a suspend function providing natural backpressure
* Wrap Flow-based observers in `lifecycleScope.launch { }` for lifecycle-aware cancellation
* Replace `runOnUiThread` with `withContext(Dispatchers.Main)`

#### Back Pressure

In Kotlin v5, back pressure is handled automatically through **coroutine suspension**. The `registerObserver` handler is a `suspend` lambda — while it is suspended or executing, Ditto will not deliver the next event. When the lambda returns, Ditto is automatically signaled to produce the next update.

<Note>
  **Coalescing behavior**: Changes that occur while your handler is executing are coalesced. When the handler completes and Ditto delivers the next callback, you receive the latest state — not every intermediate change.
</Note>

```kotlin theme={null}
// Suspension provides backpressure automatically
lifecycleScope.launch {
    ditto.store.collect(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "blue")
    ) { result ->
        // While this block is executing (or suspended) 
        // no new events are delivered
        expensiveOperation(result.items)
        withContext(Dispatchers.Main) { updateUI(result.items) }
        // When this lambda returns, Ditto delivers the next event
    }
}
```

For maximum throughput (accepting all updates as fast as possible), use the Flow-returning `observe()`:

```kotlin theme={null}
lifecycleScope.launch {
    ditto.store.observe(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "blue")
    ) { result ->
        result.items
    }.collect { items ->
        // Flow signals immediately after emit 
        // use this when you want all events
        updateUI(items)
    }
}
```

### 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("https://your-server.ditto.live"),
              )
          )
          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.

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

    <CodeGroup>
      ```kotlin Kotlin (v5) theme={null}
      val config = DittoConfig(
          databaseId = "your-database-id",
          connect = DittoConfig.Connect.Server(url = "https://your-server.ditto.live")
      )
      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("https://your-server.ditto.live"),
        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()`                                          |

### Transport Condition Changes

### Transport Condition (Replaces ditto.callback)

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

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

***
