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

# JavaScript Web V4→V5 API Migration Guide

> Guide for migrating JavaScript Web apps from Ditto v4 to v5.

## Overview

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

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

***

## AI Agent Prompt

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

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

  INITIALIZATION:
  Replace `new Ditto(identity)` with `await Ditto.open(config)` using async/await.
  - `await init()` is STILL required for web before any Ditto API calls (loads WebAssembly)
  - Create a `DittoConfig` with `databaseID` and a connect mode object
  - `{ type: 'onlinePlayground', appID, token }` → `new DittoConfig(databaseID, { mode: 'server', url: 'https://<databaseID>.cloud.ditto.live' })`
  - `{ type: 'onlineWithAuthentication', appID, authHandler }` → `new DittoConfig(databaseID, { mode: 'server', url })` + `auth.setExpirationHandler()`
  - `{ type: 'offlinePlayground', appID }` → `new DittoConfig(databaseID, { mode: 'smallPeersOnly' })`
  - `{ type: 'sharedKey', appID, sharedKey }` → `new DittoConfig(databaseID, { mode: 'smallPeersOnly', privateKey: sharedKey })`
  - `{ type: 'manual', certificate }` → **No replacement** — removed in v5
  - The Ditto constructor is now private. You MUST use `Ditto.open(config)` (async) or `Ditto.openSync(config)` (sync).
  - Remove `disableSyncWithV3()` calls — v3 protocol is fully dropped in v5
  - Remove `updateTransportConfig` workaround patterns for adding websocket URLs
  - Remove `ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")` — v5 defaults to `false`
  - IMPORTANT: If your v4 app used the default (strict=true), set `DQL_STRICT_MODE=true` BEFORE starting sync

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

  AUTHENTICATION:
  The `AuthenticationHandler` callback interface is removed. Replace with `setExpirationHandler` + `login()`.
  - Set `await ditto.auth.setExpirationHandler(async (ditto, timeUntilExpiration) => { ... })` after Ditto.open()
  - Handler signature: `(ditto: Ditto, timeUntilExpiration: number) => Promise<any> | any`
  - Inside the handler, call `await ditto.auth.login(token, provider)` which returns `{ clientInfo, error }`
  - Use `Authenticator.DEVELOPMENT_PROVIDER` for playground/dev tokens

  OBSERVERS:
  Observers use callback-based pattern with `registerObserver(query, handler, queryArguments?)`.
  - Parameter order: handler comes BEFORE query arguments
  - `observer.cancel()` for cleanup (replaces `liveQuery.stop()`)
  - For back-pressure: use `registerObserverWithSignalNext(query, handler, args?)` to control when next update fires

  COLLECTION API → DQL:
  The entire Collection/Document API is removed. Replace with `store.execute()`.
  - `store.collection(name)` → `store.execute('SELECT * FROM <name>')`
  - `collection.upsert(value)` → `store.execute('INSERT INTO <name> DOCUMENTS (:doc)', { doc: value })`
  - `collection.findByID(id).update(fn)` → `store.execute('UPDATE <name> SET ... WHERE _id = :id', { id })`
  - `collection.findByID(id).remove()` → `store.execute('DELETE FROM <name> WHERE _id = :id', { id })`
  - Always use parameterized queries with `:paramName` — NEVER use string interpolation

  LIVE QUERIES → STORE OBSERVERS:
  - `collection.findAll().observeLocal(handler)` → `store.registerObserver(query, handler, args?)`
  - `liveQuery.stop()` → `observer.cancel()`
  - v4 handler: `(docs, event) => { ... }` → v5 handler: `(result) => { ... }` where result has `.items`

  SUBSCRIPTIONS:
  - `collection.findAll().subscribe()` → `sync.registerSubscription('SELECT * FROM <name>')`
  - `Subscription` → `SyncSubscription`; generics removed

  TRANSACTIONS:
  - `store.write(callback)` → `store.transaction(scope, options?)`
  - `WriteTransaction` classes removed — use DQL via `tx.execute()`

  PRESENCE:
  - `peer.peerKeyString` → `peer.peerKey`
  - `peer.isConnectedToDittoCloud` → `peer.isConnectedToDittoServer`
  - `connection.peerKeyString1/2` → `connection.peer1/2`
  - `connection.approximateDistanceInMeters` → removed
  - `ditto.observePeers()` → `ditto.presence.observe()`

  DITTO CLASS:
  - `new Ditto(identity)` → `await Ditto.open(config)` (constructor private)
  - `ditto.appID` → `ditto.config.databaseID`
  - `ditto.siteID` → removed
  - `ditto.sdkVersion` → `Ditto.VERSION` (static)
  - `ditto.path` → `ditto.absolutePersistenceDirectory`
  - `ditto.runGarbageCollection()` → removed (automatic)
  - `ditto.close()` is still available (async)

  BEFORE (v4):
  ```typescript
  import { init, Ditto } from '@dittolive/ditto'
  await init()

  const ditto = new Ditto({
      type: 'onlinePlayground',
      appID: 'your-app-id',
      token: 'your-token',
      customAuthURL: 'https://your-server.ditto.live'
  });
  ditto.updateTransportConfig((config) => {
      config.connect.websocketURLs.add('wss://...');
  });
  ditto.store.execute('ALTER SYSTEM SET DQL_STRICT_MODE = false');
  ditto.disableSyncWithV3();
  ditto.startSync();

  const observer = ditto.store.registerObserver(query, (result) => {
      updateUI(result.items);
  }, args);
  ```

  AFTER (v5):
  ```typescript
  import { init, Ditto, DittoConfig, Authenticator } from '@dittolive/ditto'
  await init()

  const config = new DittoConfig(
      "your-database-id",
      { mode: 'server', url: "https://your-server.ditto.live" }
  );
  const ditto = await Ditto.open(config);

  await ditto.auth.setExpirationHandler(async (ditto, timeUntilExpiration) => {
      try {
          await ditto.auth.login("your-token", Authenticator.DEVELOPMENT_PROVIDER);
      } catch (error) {
          console.error(`Ditto auth failed: ${error}`);
      }
  });

  ditto.sync.start();

  const observer = ditto.store.registerObserver(query, (result) => {
      updateUI(result.items);
  }, args);

  // Cleanup
  observer.cancel();
  ditto.sync.stop();
  await ditto.close();
  ```

  CHECKLIST:
  - Ensure `await init()` is called before Ditto APIs (required for web)
  - Replace `new Ditto(identity)` with `await Ditto.open(new DittoConfig(...))`
  - Create `DittoConfig` with `databaseID` and connect mode
  - Set up `auth.setExpirationHandler()` with `(ditto, timeUntilExpiration)` signature
  - Replace `startSync()` → `sync.start()`
  - Replace all `store.collection()` calls with `store.execute()` DQL
  - Replace `store.write()` with `store.transaction()`
  - Replace `liveQuery.stop()` with `observer.cancel()`
  - Replace `ditto.observePeers()` with `ditto.presence.observe()`
  - Update `peerKeyString` → `peerKey`
  - Update `isConnectedToDittoCloud` → `isConnectedToDittoServer`
  - Remove `disableSyncWithV3()`, `updateTransportConfig` workarounds

  Work through the codebase systematically. Show me each file's changes.
  ````
</Accordion>

***

## Ditto Instance Initialization

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

<CodeGroup>
  ```typescript JAVASCRIPT (v5) theme={null}
  import { init, Ditto, DittoConfig, Authenticator } from '@dittolive/ditto'

  // Required for web — initializes the WebAssembly module
  await init()

  // 1. Configure
  const config = new DittoConfig(
      "your-database-id",
      {
          mode: 'server',
          url: "https://your-server.ditto.live"
      }
  );

  // 2. Initialize (async)
  const ditto = await Ditto.open(config);

  // 3. Authenticate
  await ditto.auth.setExpirationHandler(async (ditto, timeUntilExpiration) => {
      try {
          await ditto.auth.login("your-token", Authenticator.DEVELOPMENT_PROVIDER);
      } catch (error) {
          console.error(`Ditto auth failed: ${error}, time until expiration: ${timeUntilExpiration}`);
      }
  });

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

  ```typescript JAVASCRIPT (v4) theme={null}
  import { init, Ditto } from '@dittolive/ditto'

  await init()

  // Everything mixed together
  const ditto = new Ditto({
      type: 'onlinePlayground',
      appID: 'your-app-id',
      token: 'your-token',
      customAuthURL: 'https://your-server.ditto.live'
  });

  // Workarounds
  ditto.updateTransportConfig((config) => {
      config.connect.websocketURLs.add('wss://...');
  });
  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
* Async initialization prevents blocking the event loop
* No workarounds needed (transport config, DQL strict mode, v3 sync disable)
* Type-safe configuration with compile-time validation

<Note>
  **Web-specific**: `await init()` is still required before any Ditto API calls. It initializes the WebAssembly module. This is unchanged from v4.
</Note>

**Initialization Migration Steps**

<Steps>
  <Step title="Replace Initialization">
    Replace `new Ditto()` constructor with `await Ditto.open(config)`.

    <CodeGroup>
      ```typescript JAVASCRIPT (v5) theme={null}
      await init()  // Still required for web

      const config = new DittoConfig(
          "your-database-id",
          {
              mode: 'server',
              url: "https://your-server.ditto.live"
          }
      );

      const ditto = await Ditto.open(config);
      ditto.sync.start();
      ```

      ```typescript JAVASCRIPT (v4) theme={null}
      await init()

      const ditto = new Ditto({
          type: 'onlinePlayground',
          appID: 'your-app-id',
          token: 'your-token',
          customAuthURL: 'https://your-server.ditto.live'
      });
      ditto.startSync();
      ```
    </CodeGroup>

    **Key changes:**

    * Use `DittoConfig` constructor instead of identity object
    * `appID` → `databaseID` (first parameter)
    * `{ type: 'onlinePlayground' }` → `{ mode: 'server', url: ... }`
    * `{ type: 'offlinePlayground' }` → `{ mode: 'smallPeersOnly' }`
    * `{ type: 'sharedKey', sharedKey }` → `{ mode: 'smallPeersOnly', privateKey: sharedKey }`
    * Add `await` for async initialization
    * Remove `updateTransportConfig`, `disableSyncWithV3()`, and DQL strict mode queries
  </Step>

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

    <CodeGroup>
      ```typescript JAVASCRIPT (v5) theme={null}
      // Set handler after Ditto.open()
      // Handler signature: (ditto: Ditto, timeUntilExpiration: number) => Promise<any> | any
      await ditto.auth.setExpirationHandler(async (ditto, timeUntilExpiration) => {
              // Initial auth or token refresh
              try {
                  await ditto.auth.login(
                      "your-token",
                      Authenticator.DEVELOPMENT_PROVIDER
                  );
              } catch (error) {
                  console.error(`Ditto auth failed: ${error}`);
              }
      });
      ```

      ```typescript JAVASCRIPT (v4) theme={null}
      // Auth mixed with identity
      const ditto = new Ditto({
          type: 'onlinePlayground',
          appID: '...',
          token: '...',  // Token in identity
          customAuthURL: '...'
      });
      ```
    </CodeGroup>

    **Key changes:**

    * Authentication now uses `setExpirationHandler` async function
    * Handler receives `(ditto: Ditto, timeUntilExpiration: number)`
    * First parameter is the `Ditto` instance — access auth via `ditto.auth.login()`
    * Handler called when auth required (`timeUntilExpiration === 0`) or token expiring
    * Use `Authenticator.DEVELOPMENT_PROVIDER` for playground tokens
    * Custom auth: `"your-provider-name"` string
  </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>
      ```typescript JavaScript (v5 - Maintain v4 Behavior) highlight={7,8} theme={null}
      const config = new DittoConfig(
          "your-database-id",
          {
              mode: 'server',
              url: "https://your-server.ditto.live"
          }
      );
      const 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>
      ```typescript JavaScript (v5) theme={null}
      const config = new DittoConfig(
          "your-database-id",
          {
              mode: 'server',
              url: "https://your-server.ditto.live"
          }
      );
      const ditto = await Ditto.open(config);

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

      ```typescript JavaScript (v4 - Your Current Setup) theme={null}
      const ditto = new Ditto({
          type: 'onlinePlayground',
          appID: 'your-app-id',
          token: 'your-token',
          customAuthURL: 'https://your-server.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>
             ```typescript JavaScript (v4 - Enable Non-Strict Mode) theme={null}
             const ditto = new Ditto({
                 type: 'onlinePlayground',
                 appID: 'your-app-id',
                 token: 'your-token',
                 customAuthURL: 'https://your-server.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 [JavaScript Legacy→DQL Migration Guide](./web-js-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`

**To maintain v4 compatibility:**

```typescript theme={null}
const config = new DittoConfig(
    "your-database-id",
    {
        mode: 'server',
        url: "https://your-server.ditto.live"
    },
    "/app/data/ditto"  // Old v4 path
);
```

### API Renames

| v4.14                                          | v5.0                                                     |
| ---------------------------------------------- | -------------------------------------------------------- |
| `new Ditto(identity)` constructor              | `Ditto.open(config)` (async) or `Ditto.openSync(config)` |
| `ditto.startSync()`                            | `ditto.sync.start()`                                     |
| `ditto.stopSync()`                             | `ditto.sync.stop()`                                      |
| `ditto.isSyncActive`                           | `ditto.sync.isActive`                                    |
| `ditto.appID`                                  | `ditto.config.databaseID`                                |
| `ditto.siteID`                                 | Removed                                                  |
| `ditto.sdkVersion`                             | `Ditto.VERSION` (static)                                 |
| `ditto.observePeers(callback)`                 | `ditto.presence.observe(callback)`                       |
| `Store.write(callback)`                        | `Store.transaction(callback)`                            |
| `Store.fetchAttachment(AttachmentToken)`       | `Store.fetchAttachment(plain object)`                    |
| `registerObserver<T, U>()` (2 type params)     | `registerObserver<T>()` (1 type param)                   |
| `Sync.registerSubscription<T>()` (typed)       | `Sync.registerSubscription()` (no type param)            |
| `QueryArguments` type                          | `DQLQueryArguments` (adds `bigint` support)              |
| `SortDirection` / `WriteStrategy` exports      | Removed — use DQL equivalents                            |
| `peerKeyString`                                | `peerKey`                                                |
| `isConnectedToDittoCloud`                      | `isConnectedToDittoServer`                               |
| `connection.peerKeyString1` / `peerKeyString2` | `connection.peer1` / `peer2`                             |
| `connection.approximateDistanceInMeters`       | **REMOVED — no replacement**                             |

### `Store.write()` → `Store.transaction()`

```typescript theme={null}
// v4
await ditto.store.write(async (tx) => {
    await tx.collection('cars').upsert({ _id: 'car-1', color: 'blue' });
});

// v5
await ditto.store.transaction(async (tx) => {
    await tx.execute(
        'INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE',
        { doc: { _id: 'car-1', color: 'blue' } }
    );
});
```

### Back Pressure (registerObserverWithSignalNext)

The standard `registerObserver` automatically calls `signalNext()` after your handler returns (in a `finally` block). Use `registerObserverWithSignalNext` when your handler needs to perform async work before it is ready to receive the next update.

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

<Note>
  **Parameter order**: Both `registerObserver` and `registerObserverWithSignalNext` take `(query, handler, queryArguments?)` — the handler comes before the query arguments.
</Note>

```typescript theme={null}
// Standard observer — signalNext called automatically after handler returns
const observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    (result) => {
        updateUI(result.items);
        // signalNext() called automatically here
    },
    { color: "blue" }
);

// Back-pressure observer — call signalNext when ready for the next update
const observer = ditto.store.registerObserverWithSignalNext(
    "SELECT * FROM cars WHERE color = :color",
    async (result, signalNext) => {
        await expensiveOperation(result.items);
        signalNext();  // Ready for the next update
    },
    { color: "blue" }
);

observer.cancel(); // when done
```

**When to use signal next:**

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

***

## APIs Removed

| Removed API                                                                  | Notes                                                |
| ---------------------------------------------------------------------------- | ---------------------------------------------------- |
| `new Ditto()` constructor                                                    | Use `Ditto.open(config)` or `Ditto.openSync(config)` |
| `ditto.disableSyncWithV3()`                                                  | v3 protocol support fully dropped                    |
| `ditto.runGarbageCollection()`                                               | GC is now automatic                                  |
| `ditto.siteID`                                                               | Removed entirely                                     |
| `ditto.observePeers()`                                                       | Use `ditto.presence.observe()`                       |
| `Document` / `MutableDocument`                                               | DQL returns plain objects                            |
| `DocumentID` / `DocumentPath` / `MutableDocumentPath`                        | Removed with legacy collection API                   |
| `Counter` / `Register` CRDT types                                            | Use DQL `PN_INCREMENT` for counters                  |
| `Collection` / `PendingCursorOperation`                                      | Use `store.execute()` with DQL                       |
| `LiveQuery` / `LiveQueryEvent`                                               | Use `store.registerObserver()`                       |
| `WriteTransaction` / `WriteTransactionCollection` / `WriteTransactionResult` | Use `store.transaction()` with DQL                   |
| `UpdateResult` / `UpdateResultsMap`                                          | DQL returns row counts                               |
| `PresenceManager`                                                            | Use `ditto.presence` directly                        |
| `SortDirection` / `WriteStrategy`                                            | Use DQL equivalents                                  |
| `AttachmentToken` class                                                      | Use plain `{ id, len, metadata }` objects            |
| `Address` / `addressToString()`                                              | Removed entirely                                     |
| `auth.loginWithToken()` / `auth.loginWithUsernameAndPassword()`              | Use `auth.login()`                                   |
| `Logger.setLogFile()` / `Logger.setLogFileURL()`                             | Removed                                              |
| `Logger.emojiLogLevelHeadingsEnabled`                                        | Removed                                              |
| `connection.approximateDistanceInMeters`                                     | Removed                                              |

***

## New APIs in v5

* **`Ditto.open(config): Promise<Ditto>`** — async factory method
* **`Ditto.openSync(config): Ditto`** — synchronous factory method
* **`DittoConfig(id, connect, persistenceDir?)`** — replaces identity system
* **`{ mode: 'server', url }` / `{ mode: 'smallPeersOnly', privateKey? }`** — connect mode objects
* **`auth.login(token, provider): Promise<LoginResult>`** — returns `{ clientInfo, error }`
* **`Authenticator.DEVELOPMENT_PROVIDER`** — static constant for dev provider
* **`auth.observeStatus(callback): Observer`** — observe auth status changes
* **`store.transaction(scope, options?)`** — DQL-based transaction
* **`store.registerObserverWithSignalNext(query, handler, args?)`** — back-pressure control
* **`StoreObserver.queryArgumentsCBORData`** / **`.queryArgumentsJSONString`** — introspection
* **`SyncSubscription.queryArgumentsCBORData`** / **`.queryArgumentsJSONString`** — introspection
* **`Peer.isCompatible`** — optional peer compatibility flag
* **`Peer.isConnectedToDittoServer`** — replaces `isConnectedToDittoCloud`
* **`Ditto.VERSION`** (static) — replaces `ditto.sdkVersion`
* **`ditto.absolutePersistenceDirectory`** — replaces `ditto.path` / `ditto.persistenceDirectory`
* **`QueryResult.mutatedDocumentIDsV2()`** — returns `any[]`

***

## Migration Checklist

### Initialization

* [ ] Ensure `await init()` is called before Ditto APIs (still required for web)
* [ ] Replace `new Ditto()` with `await Ditto.open(config)`
* [ ] Create `DittoConfig` with `databaseID` and `connect` mode
* [ ] Add `await` for async initialization
* [ ] Update `{ type: 'onlinePlayground' }` → `{ mode: 'server', url: ... }`
* [ ] Update `{ type: 'offlinePlayground' }` → `{ mode: 'smallPeersOnly' }`
* [ ] Update `{ type: 'sharedKey', sharedKey }` → `{ mode: 'smallPeersOnly', privateKey: sharedKey }`
* [ ] Remove `updateTransportConfig` calls
* [ ] Remove `disableSyncWithV3()` calls
* [ ] **Set `DQL_STRICT_MODE=true` BEFORE starting sync if maintaining v4 behavior**
* [ ] Update `startSync()` → `sync.start()`

### Observers

* [ ] Replace `collection.findAll().observeLocal(handler)` with `store.registerObserver(query, handler, args?)`
* [ ] Observer callbacks now receive `QueryResult` with `.items` — update handler signatures
* [ ] Replace `liveQuery.stop()` with `observer.cancel()`
* [ ] Note parameter order: `registerObserver(query, handler, queryArguments?)`
* [ ] If processing is expensive, use `registerObserverWithSignalNext` for back-pressure control

### Authentication

* [ ] Set `setExpirationHandler` async function after initialization
* [ ] Handler receives `(ditto: Ditto, timeUntilExpiration: number)`
* [ ] Use `Authenticator.DEVELOPMENT_PROVIDER` for playground tokens
* [ ] Replace `auth.loginWithToken()` with `auth.login()`
* [ ] Remove authentication from identity configuration

### Data Operations

* [ ] Replace all `store.collection('x')` calls with `store.execute()` DQL
* [ ] Use parameterized queries with `:paramName` — never string interpolation
* [ ] Replace `.upsert()` with `INSERT INTO ... DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE`
* [ ] Replace `.update()` closures with `UPDATE SET` DQL
* [ ] Replace `.remove()` with `DELETE FROM`
* [ ] Replace `.evict()` with `EVICT FROM`
* [ ] Replace `.subscribe()` with `sync.registerSubscription()`
* [ ] Replace `store.write()` with `store.transaction()`

### Breaking Changes

* [ ] Set `persistenceDirectory` if maintaining v4 directory structure
* [ ] Update `peerKeyString` → `peerKey`
* [ ] Update `isConnectedToDittoCloud` → `isConnectedToDittoServer`
* [ ] Update `connection.peerKeyString1/2` → `connection.peer1/2`
* [ ] Replace `ditto.observePeers()` with `ditto.presence.observe()`
* [ ] Replace `AttachmentToken` instances in `fetchAttachment()` with plain objects
* [ ] Update `registerObserver<T,U>()` → `registerObserver<T>()` (remove second type param)
* [ ] Update `registerSubscription<T>()` → `registerSubscription()` (remove type param)
* [ ] Rename `QueryArguments` → `DQLQueryArguments`
* [ ] Remove imports of `SortDirection`, `WriteStrategy`, `Counter`, `Register`
* [ ] Update `BigInt` type annotations to lowercase `bigint`
* [ ] Remove `Logger.setLogFile()` / `Logger.emojiLogLevelHeadingsEnabled` calls
* [ ] Remove `DittoError.context` usage
* [ ] Set `websocketSync = true` explicitly if your app relied on the v4 default of `true`

### Verification

* [ ] Build compiles with zero errors
* [ ] No deprecated API warnings
* [ ] Observers update UI immediately
* [ ] Authentication works before sync starts
* [ ] WebAssembly module loads correctly
* [ ] No memory leaks on navigation

***

## Common Pitfalls

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

2. **Missing `await init()`**: For web apps, `init()` is still required before any Ditto API calls. It loads the WebAssembly module. Forgetting it will cause runtime errors.

3. **Missing `await` on `Ditto.open()`**: `Ditto.open()` returns a Promise. Forgetting `await` means you're working with a Promise, not a Ditto instance.

4. **String interpolation in queries**: Never `` `SELECT * FROM cars WHERE color = '${color}'` ``. Always use parameterized queries: `store.execute("SELECT * FROM cars WHERE color = :color", { color })`.

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

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

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