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

# Flutter V4→V5 API Migration Guide

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

## Overview

This guide covers the essential changes needed to migrate your Ditto Flutter app from v4 to v5. The main architectural shift is moving from identity-based initialization to `DittoConfig`, plus several breaking type changes.

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

***

## AI Agent Prompt

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

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

  INITIALIZATION:
  Replace `Ditto.open(identity: Identity)` with `Ditto.open(DittoConfig(...))`:
  - `DittoConfig(databaseID: String, connect: DittoConfigConnect, ...)` replaces identity types
  - `DittoConfigConnectServer(url: String)` replaces `OnlinePlaygroundIdentity` — URL is a plain `String`, NOT a `Uri`
  - `DittoConfigConnectSmallPeersOnly(privateKey: String?)` replaces `OfflinePlaygroundIdentity` and `SharedKeyIdentity`
  - `Ditto.openSync(config)` available as synchronous alternative
  - `await Ditto.init()` must be called before any Ditto usage
  - Remove `updateTransportConfig` WebSocket URL additions (cloud URL inferred from config)
  - Remove `DQL_STRICT_MODE = false` workaround (now the v5 default)
  - If your v4 app relied on `DQL_STRICT_MODE = true` (v4 default), set it BEFORE `sync.start()`:
    `await 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`

  AUTHENTICATION:
  Set `expirationHandler` via async method after initialization — replaces `AuthenticationHandler` interface:
  - `await ditto.auth.setExpirationHandler((Ditto ditto, Duration timeUntilExpiration) { ... })`
  - `ditto.auth` is non-nullable — do NOT use `ditto.auth?.`
  - Handler receives `Ditto` (not `Authenticator`) and `Duration` (not `int`)
  - Called when `timeUntilExpiration == Duration.zero` (initial auth or expired) or `> 0` (token expiring)
  - Use `Authenticator.developmentProvider` for playground tokens
  - `ditto.auth.login(token: '...', provider: '...')` — returns `Future<AuthResponse>`
  - Remove `loginWithCredentials()` — use token-based `login()` instead
  - Remove `AuthenticationHandler` class/interface

  TRANSPORT CONFIG:
  Changed from mutable to immutable with builder pattern:
  - `ditto.updateTransportConfig((builder) { builder.setAllPeerToPeerEnabled(true); })`
  - Or: `ditto.transportConfig = ditto.transportConfig.withAllPeerToPeerEnabled(true);`
  - `TransportConfig.withBigPeer(...)` removed — use `DittoConfig` server config

  PRESENCE & PEERS:
  - `PresenceGraph.remotePeers` changed from `List<Peer>` to `Set<Peer>` — use `.first` instead of `[0]`
  - `Peer.peerKeyString` → `Peer.peerKey: String` (unified)
  - `Peer.isConnectedToDittoCloud` → `Peer.isConnectedToDittoServer`
  - `Connection.peerKeyString1/peerKeyString2` → `Connection.peer1/peer2`
  - `ConnectionRequest.peerKeyString` → `ConnectionRequest.peerKey`

  OBSERVERS:
  - `registerObserver()` API is the same — callback-based with `onChange` parameter
  - Observers also expose `Stream<QueryResult> changes` for reactive use
  - No `signalNext` — backpressure handled internally by SDK

  OTHER:
  - `ditto.persistenceDirectory` → `ditto.absolutePersistenceDirectory`
  - `HttpListenConfig.websocketSync` default changed from `true` to `false`
  - `Ditto` and `Store` cannot be passed across Dart isolates
  - `DittoLogger.setLogFile()` removed
  - `SmallPeerInfoSyncScope` enum removed — use DQL `ALTER SYSTEM SET sync_scope`
  - `SiteID` and `DocumentID` are no longer exported (internal types)
  - Ditto Flutter SDK for MacOS now targets 12.0+ - if your project targets older releases you will need to raise the minimum version.
  - Ditto Flutter SDK for iOS now targets 14.0+ - if your project targets older releases you will need to raise the minimum version.
  - MacOS and iOS use CocoaPods for dependency management, because of this you will probably need to delete the Pod lock file and run `pod install` again.  This is a standard CocoaPods issue when upgrading dependencies.
  - Ditto Flutter SDK for Android minimum SDK version is now 24 (Android 7.0 Nougat). If your project targets `minSdk = 23`, you must raise it.
  ```
</Accordion>

***

## Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control. Note that `Ditto.init()` is a prerequisite step that must be called before any Ditto usage:

<CodeGroup>
  ```dart DART (v5) theme={null}
  import 'package:ditto_live/ditto_live.dart';

  // 1. Initialize the Ditto system (required before any Ditto usage)
  await Ditto.init();

  // 2. Configure
  final config = DittoConfig(
      databaseID: 'your-database-id',
      connect: DittoConfigConnectServer(
          url: 'https://your-database-id.cloud.ditto.live',
      ),
  );

  // 3. Open
  final ditto = await Ditto.open(config);
  // OR synchronous: final ditto = Ditto.openSync(config);

  // 4. Authenticate (required for server connections)
  await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) {
      ditto.auth.login(
          token: 'your-token',
          provider: Authenticator.developmentProvider,
      );
  });

  // 5. Start sync
  ditto.sync.start();
  ```

  ```dart DART (v4) theme={null}
  import 'package:ditto_live/ditto_live.dart';

  await Ditto.init();

  final identity = OnlinePlaygroundIdentity(
      appID: 'your-app-id',
      token: 'your-token',
      customAuthUrl: 'https://your-app-id.cloud.ditto.live',
      enableDittoCloudSync: false,
  );
  final ditto = await Ditto.open(identity: identity);

  ditto.updateTransportConfig((config) {
   config.setAllPeerToPeerEnabled(true);
   config.connect.webSocketUrls.add('your websocket url');
  });

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

**These changes provide:**

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

**Initialization Migration Steps**

<Steps>
  <Step title="Replace Initialization">
    Replace `Ditto.open(identity:)` with `Ditto.open(DittoConfig)`.

    <CodeGroup>
      ```dart DART (v5) theme={null}
      final config = DittoConfig(
          databaseID: 'your-database-id',
          connect: DittoConfigConnectServer(
              url: 'https://your-database-id.cloud.ditto.live',
          ),
      );
      final ditto = await Ditto.open(config);
      ditto.sync.start();
      ```

      ```dart DART (v4) theme={null}
      final ditto = await Ditto.open(
          identity: OnlinePlaygroundIdentity(
              appID: 'your-app-id',
              token: 'your-token',
              enableDittoCloudSync: false,
          ),
      );
      ditto.startSync();
      ```
    </CodeGroup>

    **Identity → DittoConfig mapping:**

    | v4.14 Identity                                           | v5.0 DittoConfig connect                             |
    | -------------------------------------------------------- | ---------------------------------------------------- |
    | `OnlinePlaygroundIdentity(appID:token:customAuthUrl:)`   | `DittoConfigConnectServer(url:)`                     |
    | `OnlineWithAuthenticationIdentity(appID:customAuthUrl:)` | `DittoConfigConnectServer(url:)` + auth handler      |
    | `OfflinePlaygroundIdentity(appID:siteID:)`               | `DittoConfigConnectSmallPeersOnly(privateKey: null)` |
    | `SharedKeyIdentity(appID:sharedKey:siteID:)`             | `DittoConfigConnectSmallPeersOnly(privateKey: key)`  |

    **Key changes:**

    * Use `DittoConfig` with `databaseID` instead of identity types
    * `appID` → `databaseID`
    * Connect mode selected via `DittoConfigConnect*` subclasses
    * `DittoConfigConnectServer.url` takes a `String`, not a `Uri`
    * New: `Ditto.openSync(config)` synchronous alternative
    * `ditto.sync.start()` instead of `ditto.startSync()`
  </Step>

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

    <CodeGroup>
      ```dart DART (v5) theme={null}
      // Set handler after Ditto.open() — note: setExpirationHandler is async
      await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) {
          // Called for initial auth (timeUntilExpiration == Duration.zero)
          // and when token is expiring
          ditto.auth.login(
              token: 'your-token',
              provider: Authenticator.developmentProvider,
          );
      });
      ```

      ```dart DART (v4) theme={null}
      // Auth mixed with identity (OnlineWithAuthentication)
      final identity = OnlineWithAuthenticationIdentity(
          appID: 'your-app-id',
          authenticationHandler: AuthenticationHandler(
              authenticationRequired: (authenticator) {
                  authenticator.login(
                      token: 'your-token',
                      provider: 'your-provider',
                  );
              },
              authenticationExpiringSoon: (authenticator, remaining) { },
          ),
          customAuthUrl: 'https://your-app-id.cloud.ditto.live',
      );
      ```
    </CodeGroup>

    **Key changes:**

    * `AuthenticationHandler` interface removed — use `AuthenticationExpirationHandler` typedef
    * Set handler via `await ditto.auth.setExpirationHandler(...)` (async method, not property assignment)
    * `ditto.auth` is non-nullable in v5 (no `?.` needed)
    * Handler signature: `void Function(Ditto ditto, Duration timeUntilExpiration)` — receives `Ditto` instance and `Duration`, not `Authenticator` and `int`
    * `Authenticator.loginWithCredentials(username:password:provider:)` removed — use token-based `login(token:provider:)`
    * `Authenticator.developmentProvider` is the dev provider constant
  </Step>
</Steps>

***

## Additional Changes

### DQL Strict Mode Behavior Change

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

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

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

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

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

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

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

    <CodeGroup>
      ```dart DART (v5 - Maintain v4 Behavior) highlight={6,7} theme={null}
      final config = DittoConfig(
          databaseID: 'your-database-id',
          connect: DittoConfigConnectServer(
              url: 'https://your-database-id.cloud.ditto.live',
          ),
      );
      final ditto = await Ditto.open(config);

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

      // Now safe to start sync
      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>
      ```dart DART (v5) theme={null}
      final config = DittoConfig(
          databaseID: 'your-database-id',
          connect: DittoConfigConnectServer(
              url: 'https://your-database-id.cloud.ditto.live',
          ),
      );
      final ditto = await Ditto.open(config);

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

      ```dart DART (v4 - Your Current Setup) theme={null}
      final ditto = await Ditto.open(
          identity: OnlinePlaygroundIdentity(
              appID: 'your-app-id',
              token: 'your-token',
              enableDittoCloudSync: false,
              customAuthUrl: 'https://your-app-id.cloud.ditto.live',
          ),
      );

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

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

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

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

    **Migration Steps**:

    1. **In v4: Set `DQL_STRICT_MODE=false`** after initialization:
           <CodeGroup>
             ```dart DART (v4 - Enable Non-Strict Mode) theme={null}
             final ditto = await Ditto.open(
                 identity: OnlinePlaygroundIdentity(
                     appID: 'your-app-id',
                     token: 'your-token',
                     enableDittoCloudSync: false,
                     customAuthUrl: 'https://your-app-id.cloud.ditto.live',
                 ),
             );

             // Set DQL_STRICT_MODE to false for Query Builder compatibility
             await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false");

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

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

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

For additional guidance or questions, contact Ditto Customer Support.

### Default Persistence Directory

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

**Provide custom persistence directory:**

```dart theme={null}
final config = DittoConfig(
    databaseID: 'your-database-id',
    connect: DittoConfigConnectServer(
        url: 'https://your-database-id.cloud.ditto.live',
    ),
    persistenceDirectory: '/app/data/ditto',  // custom path to preserve v4 data
);
```

### Observer Changes

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

<CodeGroup>
  ```dart DART (v5) theme={null}
  final observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      arguments: {"color": "red"},
      onChange: (result) {
          setState(() { _cars = result.items; });
      },
  );

  // Cleanup
  observer.cancel();

  // Alternative: use the Stream-based API
  final observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      arguments: {"color": "red"},
  );
  observer.changes.listen((result) {
      setState(() { _cars = result.items; });
  });
  ```

  ```dart DART (v4) theme={null}
  final observer = ditto.store.registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      arguments: {"color": "red"},
      onChange: (result) {
          setState(() { _cars = result.items; });
      },
  );

  // Cleanup
  observer.cancel();
  ```
</CodeGroup>

**Key changes:**

* The observer lifecycle pattern (retain, `.cancel()`, cleanup) is the same as v4
* v5 observers expose a `Stream<QueryResult> changes` property for reactive use
* `StoreObserver.queryString` and `queryArguments` properties available for introspection

### Back Pressure

The Flutter SDK handles back pressure **internally** through its `Stream<QueryResult>`-based observer model. There is no `signalNext` function to call — the SDK manages update delivery automatically.

```dart theme={null}
// Observers use Stream internally — backpressure is handled by the SDK
final observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    arguments: {"color": "blue"},
    onChange: (result) {
        setState(() { _cars = result.items; });
    },
);
```

If you need fine-grained flow control, use a Dart `StreamController` with `pause` / `resume` on the observer's stream.

### `PresenceGraph.remotePeers` Type Changed

<Warning>
  **Breaking Change**: `PresenceGraph.remotePeers` changed type from `List<Peer>` to `Set<Peer>`.
</Warning>

```dart theme={null}
// v4 — List, index access works
final first = graph.remotePeers[0];

// v5 — Set, use .first
final first = graph.remotePeers.first;
// Or: graph.remotePeers.toList()[0]
```

### WebSocket Sync Default Changed

<Warning>
  `HttpListenConfig.websocketSync` now defaults to `false` (was `true` in v4). If your app relied on WebSocket sync being enabled automatically, you must now enable it explicitly.
</Warning>

### TransportConfig Now Immutable

`TransportConfig` and all its sub-types are now `@immutable`. Use the builder pattern or the `updateTransportConfig` convenience method:

```dart theme={null}
// v5 — Using updateTransportConfig (recommended)
ditto.updateTransportConfig((builder) {
    builder.setAllPeerToPeerEnabled(true);
});

// v5 — Using builder pattern directly
final newConfig = ditto.transportConfig.toBuilder()
    ..setAllPeerToPeerEnabled(true);
ditto.transportConfig = newConfig.build();

// v5 — Using immutable copy method
ditto.transportConfig = ditto.transportConfig.withAllPeerToPeerEnabled(true);
```

```dart theme={null}
// v4 — TransportConfig was mutable
ditto.updateTransportConfig((config) {
    config.setAllPeerToPeerEnabled(true);
    config.connect.webSocketUrls.add('wss://...');
});
```

### Isolate Restriction

`Ditto` and `Store` are marked `@pragma("vm:isolate-unsendable")`. They cannot be passed across Dart isolates.

***

## API Renames

| v4.14                                                             | v5.0                                               |
| ----------------------------------------------------------------- | -------------------------------------------------- |
| `ditto.startSync()`                                               | `ditto.sync.start()`                               |
| `ditto.stopSync()`                                                | `ditto.sync.stop()`                                |
| `ditto.isSyncActive`                                              | `ditto.sync.isActive`                              |
| `Peer.peerKeyString` + `Peer.peerKey: Uint8List`                  | `Peer.peerKey: String` (unified)                   |
| `Peer.isConnectedToDittoCloud`                                    | `Peer.isConnectedToDittoServer`                    |
| `Connection.peerKeyString1` / `peerKeyString2`                    | `Connection.peer1` / `Connection.peer2`            |
| `ConnectionRequest.peerKeyString`                                 | `ConnectionRequest.peerKey`                        |
| `ditto.persistenceDirectory`                                      | `ditto.absolutePersistenceDirectory`               |
| `SmallPeerInfo.syncScope: SmallPeerInfoSyncScope`                 | DQL: `ALTER SYSTEM SET sync_scope = '...'`         |
| `SmallPeerInfoSyncScope` enum                                     | Removed — use DQL                                  |
| `AuthenticationHandler` interface                                 | `AuthenticationExpirationHandler` typedef          |
| `Authenticator.loginWithCredentials(username:password:provider:)` | Removed — use token-based `login(token:provider:)` |
| `TransportConfig.withBigPeer(...)`                                | Removed — use `DittoConfig` server configuration   |
| `DittoLogger.setLogFile(...)`                                     | Removed — file logging no longer supported         |
| `HttpListenConfig.staticContentPath`                              | Removed — no replacement                           |
| `PresenceGraph.remotePeers: List<Peer>`                           | `PresenceGraph.remotePeers: Set<Peer>`             |
| `SiteID` class                                                    | Not exported (internal type)                       |
| `DocumentID` class                                                | Not exported (internal type)                       |

***

## Key Initialization Changes

| v4.14                                             | v5.0                                    | Notes                                           |
| ------------------------------------------------- | --------------------------------------- | ----------------------------------------------- |
| `Ditto.open(identity: Identity)`                  | `Ditto.open(DittoConfig)`               | Positional parameter, `DittoConfig` has default |
| `Identity` subclasses                             | `DittoConfigConnect*` subclasses        | See identity mapping table                      |
| `OnlinePlaygroundIdentity.customAuthUrl: String?` | `DittoConfigConnectServer(url: String)` | URL is now a `String`, not `Uri`                |
| `OnlinePlaygroundIdentity.enableDittoCloudSync`   | Removed                                 | Cloud sync is configured via connect mode       |

## Authentication API Changes

| v4.14                                                             | v5.0                                                 | Notes                                  |
| ----------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------- |
| `AuthenticationHandler` interface                                 | `AuthenticationExpirationHandler` typedef            | `void Function(Ditto, Duration)`       |
| `authenticationRequired(Authenticator)`                           | `expirationHandler` called with `Duration.zero`      | Single handler replaces two callbacks  |
| `authenticationExpiringSoon(Authenticator, int)`                  | `expirationHandler` called with remaining `Duration` | Combined into one handler              |
| `authenticator.loginWithCredentials(username:password:provider:)` | Removed                                              | Use `login(token:provider:)`           |
| `authenticator.login(token:provider:)`                            | `ditto.auth.login(token:provider:)`                  | Same API, accessed from `Ditto.auth`   |
| Set handler via identity constructor                              | `await ditto.auth.setExpirationHandler(...)`         | Async method, set after `Ditto.open()` |

***

## APIs Removed Without Replacement

| Removed API                              | Notes                                          |
| ---------------------------------------- | ---------------------------------------------- |
| `Connection.approximateDistanceInMeters` | Peer distance estimation removed               |
| `DittoLogger.setLogFile(...)`            | File logging removed entirely                  |
| `HttpListenConfig.staticContentPath`     | Static HTTP file serving removed               |
| `TransportConfig.withBigPeer(...)`       | Use `DittoConfig` server config instead        |
| `SmallPeerInfoSyncScope` enum            | Sync scope is now a DQL `ALTER SYSTEM` setting |
| `ManualIdentity`                         | Manual certificate identity removed            |

***

## New APIs in v5

* **`Ditto.init()`** — required static initialization before using any Ditto features (throws if not called)
* **`DittoConfig`** / **`DittoConfigConnectServer`** / **`DittoConfigConnectSmallPeersOnly`** — new configuration hierarchy replacing all identity types
* **`Ditto.openSync(config)`** — synchronous constructor alternative to async `Ditto.open(config)`
* **`Ditto.defaultRootDirectory`** — static helper for default persistence path
* **`ditto.config`** — access the config object used to open the instance
* **`AuthenticationExpirationHandler`** typedef — replaces `AuthenticationHandler` interface
* **`Authenticator.developmentProvider`** — constant for development auth provider
* **`Authenticator.setExpirationHandler()`** — async method to set the expiration handler
* **`SyncSubscription.queryArgumentsCborBytes`** / **`queryArgumentsJsonString`** — inspect subscription query arguments
* **Builder types** for all transport config sub-types (`TransportConfigBuilder`, `PeerToPeerBuilder`, etc.)
* **`TransportConfig.withAllPeerToPeerEnabled(bool)`** — immutable copy method for enabling P2P transports
* `Ditto` and `Store` marked `@pragma("vm:isolate-unsendable")` for safety

***

## Migration Checklist

### Initialization

* [ ] Ensure `await Ditto.init()` is called before any Ditto usage
* [ ] Replace `Ditto.open(identity:)` with `Ditto.open(DittoConfig(...))`
* [ ] Create `DittoConfig` with `databaseID` and `connect` mode
* [ ] Update identity types to `DittoConfigConnect*` variants
* [ ] Use `String` URLs (not `Uri`) for `DittoConfigConnectServer`
* [ ] Remove `updateTransportConfig` calls for WebSocket URLs (cloud URL inferred from config)
* [ ] **Set `DQL_STRICT_MODE=true` BEFORE `sync.start()` if maintaining v4 behavior**
* [ ] Update `startSync()` → `sync.start()`

### Authentication

* [ ] Replace `AuthenticationHandler` interface with `AuthenticationExpirationHandler` typedef
* [ ] Set handler via `await ditto.auth.setExpirationHandler(...)` after `Ditto.open()`
* [ ] Update handler signature: receives `(Ditto ditto, Duration timeUntilExpiration)`, not `(Authenticator, int)`
* [ ] Replace `loginWithCredentials(username:password:provider:)` with `login(token:provider:)`
* [ ] Use `Authenticator.developmentProvider` for dev tokens
* [ ] Remove `?.` on `ditto.auth` — it is non-nullable in v5

### Breaking Changes

* [ ] Update `remotePeers[0]` → `remotePeers.first` (type changed from `List` to `Set`)
* [ ] Check `HttpListenConfig.websocketSync` — must be explicitly enabled now (default is `false`)
* [ ] Update `TransportConfig` usage to builder pattern: `.toBuilder()...build()` or `updateTransportConfig()`
* [ ] Update `peerKeyString` → `peerKey`
* [ ] Update `isConnectedToDittoCloud` → `isConnectedToDittoServer`
* [ ] Update `persistenceDirectory` → `absolutePersistenceDirectory`
* [ ] Do not pass `Ditto` or `Store` across isolates
* [ ] Set `persistenceDirectory` in `DittoConfig` if maintaining v4 directory structure

### Verification

* [ ] Build compiles with zero errors
* [ ] Observers update data immediately
* [ ] Authentication works before sync starts
* [ ] No `remotePeers` index access errors
* [ ] WebSocket sync works if needed (explicitly enabled)

***
