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

# Node.js V4→V5 API Migration Guide

> Guide for migrating Node.js apps from Ditto v4 to v5.

## Overview

This guide covers the essential changes needed to migrate your Ditto JavaScript 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](./node-js-dql) for query migration steps.

***

## AI Agent Prompt

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

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

  INITIALIZATION:
  Replace `new Ditto(identity, path)` with `await Ditto.open(config)` using async/await.
  - 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 — `DittoConfig` handles the server URL directly. Note: `updateTransportConfig()` itself still exists in v5 for advanced transport configuration
  - 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 to maintain v4 behavior

  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
  - The handler is called for both initial auth and token refresh — check `timeUntilExpiration`
  - `auth.loginWithToken(token, provider)` → removed, use `auth.login(token, provider)` instead
  - `auth.loginWithUsernameAndPassword(u, p, provider)` → removed, use `auth.login(token, provider)` instead
  - New: `auth.observeStatus(callback)` lets you observe auth status changes

  OBSERVERS:
  The `registerObserver` API is callback-based with simplified generic type parameters.
  - `StoreObserver<T, U>` → `StoreObserver` (single type param `<T>` optional)
  - `observer.cancel()` is unchanged
  - `StoreObserver.liveQueryID` property is removed
  - IMPORTANT: Parameter order changed — `registerObserver(query, handler, queryArguments?)` — handler comes BEFORE args

  COLLECTION API → DQL:
  The entire Collection/Document API is removed. Replace all collection operations with DQL via `store.execute()`.
  - `store.collection(name)` → removed. Use `store.execute('SELECT * FROM <name>')`
  - `store.collections()` → removed
  - `store.collectionNames()` → removed, no replacement
  - `collection.findAll().exec()` → `store.execute('SELECT * FROM <name>')`
  - `collection.find(query).exec()` → `store.execute('SELECT * FROM <name> WHERE ...')`
  - `collection.findByID(id).exec()` → `store.execute('SELECT * FROM <name> WHERE _id = :id', { id })`
  - `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 })`
  - `collection.findByID(id).evict()` → `store.execute('EVICT 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('SELECT * FROM <name>', handler)`
  - `liveQuery.stop()` → `observer.cancel()`
  - The `LiveQuery` class, `LiveQueryEvent` type, and the entire callback shape are different:
    - v4 handler: `(docs, event) => { ... }` where docs is an array of Document objects
    - v5 handler: `(result) => { ... }` where result is a QueryResult with `.items` property containing plain objects

  SUBSCRIPTIONS:
  - `collection.findAll().subscribe()` → `sync.registerSubscription('SELECT * FROM <name>')`
  - `Subscription` class → replaced by `SyncSubscription`
  - `SyncSubscription<T>` generics removed → plain `SyncSubscription`
  - `syncSubscription.queryArgumentsCBOR` → renamed to `syncSubscription.queryArgumentsCBORData`

  TRANSACTIONS:
  - `store.write(callback)` → `store.transaction(scope, options?)`
  - v4: `txn.scoped('name').upsert(value)` → v5: `txn.execute('INSERT INTO <name> DOCUMENTS (:doc)', { doc: value })`
  - `WriteTransaction` / `WriteTransactionCollection` / `WriteTransactionResult` classes are all removed

  PRESENCE:
  - `peer.peerKeyString` → `peer.peerKey` (peerKey is now a string directly, not Uint8Array)
  - `peer.peerKey` (was Uint8Array) → `peer.peerKey` (now string)
  - `peer.address` → removed (Address type removed entirely)
  - `peer.isConnectedToDittoCloud` → `peer.isConnectedToDittoServer`
  - `peer.isCompatible` → new optional boolean property
  - `connection.peerKeyString1` / `peerKeyString2` → `connection.peer1` / `connection.peer2` (now strings)
  - `connection.approximateDistanceInMeters` → removed
  - `connectionRequest.peerKeyString` → `connectionRequest.peerKey`
  - `ditto.observePeers(callback)` → `ditto.presence.observe(callback)`
  - `Address` type / `addressToString()` → removed
  - `PresenceManager` class → removed

  ATTACHMENTS:
  - `AttachmentToken` class → replaced by plain objects `{ id: string, len: number | bigint, metadata }`
  - `TypedAttachmentToken` → renamed to `TypedAttachmentTokenV1` (marked `@internal`)
  - `Attachment.len` type: `number | BigInt` → `number | bigint` (lowercase)
  - `fetchAttachment(AttachmentToken, handler)` → `fetchAttachment({ id, len, metadata }, handler)` (plain object only)
  - `AttachmentFetcher.attachment` promise: v4 could resolve to `null` on deletion → v5 rejects with `DittoError` instead

  DITTO CLASS:
  - `new Ditto(identity, path)` → `await Ditto.open(config)` or `Ditto.openSync(config)` (constructor private)
  - `ditto.appID` → `ditto.config.databaseID`
  - `ditto.identity` → `ditto.config`
  - `ditto.path` / `ditto.persistenceDirectory` → `ditto.absolutePersistenceDirectory`
  - `ditto.siteID` → removed
  - `ditto.sdkVersion` → use `Ditto.VERSION` (static property)
  - `ditto.runGarbageCollection()` → removed (automatic in v5)
  - `ditto.disableSyncWithV3()` → removed (v3 protocol dropped)
  - `ditto.observePeers(callback)` → `ditto.presence.observe(callback)`

  LOGGER:
  - `Logger.setLogFile(path)` → removed
  - `Logger.setLogFileURL(url)` → removed
  - `Logger.emojiLogLevelHeadingsEnabled` → removed
  - `Logger.exportToFile(path)` — still available
  - `Logger.enabled` / `Logger.minimumLogLevel` / `Logger.setCustomLogCallback()` — still available

  ERRORS:
  - `DittoError(code, message, context)` → `DittoError(code, message)` — context parameter removed
  - `DittoError.context` property → removed
  - Error code `'sdk/unsupported'` → `'unsupported'`
  - Error code `'internal/unknown-error'` → `'unknown'`

  TRANSPORT CONFIG:
  - `TransportConfigListenHTTP.staticContentPath` → removed
  - Default `websocketSync` changed from `true` to `false` — explicitly set `true` if your app needs it

  TYPES:
  - `BigInt` → `bigint` (lowercase) across the entire SDK
  - `QueryResult.mutatedDocumentIDs()` → removed, use `QueryResult.mutatedDocumentIDsV2()` which returns `any[]`
  - `StoreObserver<T, U>` → `StoreObserver` (no generics)
  - `SyncSubscription<T>` → `SyncSubscription` (no generics)

  REMOVED CLASSES (no replacement):
  - `Document` / `MutableDocument` — DQL returns plain objects
  - `DocumentID` / `DocumentPath` / `MutableDocumentPath`
  - `Counter` / `Register` CRDT types
  - `Collection` / `PendingCursorOperation` / `PendingIDSpecificOperation` / `PendingCollectionsOperation`
  - `LiveQuery` / `LiveQueryEvent`
  - `WriteTransaction` / `WriteTransactionCollection` / `WriteTransactionResult`
  - `UpdateResult` / `UpdateResultsMap`
  - `PresenceManager`

  NEW APIs:
  - `Ditto.open(config): Promise<Ditto>` — async factory
  - `Ditto.openSync(config): Ditto` — sync factory
  - `DittoConfig(id, connect, persistenceDir?)` — replaces Identity system
  - `DittoConfig.DEFAULT_DATABASE_ID` / `.default` / `.copy()`
  - `DittoConfigConnectServer` — `{ mode: 'server', url: string }`
  - `DittoConfigConnectSmallPeersOnly` — `{ mode: 'smallPeersOnly', privateKey?: string }`
  - `auth.login(token, provider): Promise<LoginResult>` — returns `{ clientInfo, error }`
  - `Authenticator.DEVELOPMENT_PROVIDER` — static constant for dev provider
  - `auth.observeStatus(callback): Observer` — observe auth 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`

  BEFORE (v4):
  ```typescript
  import { 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',
    },
    '/data/ditto'
  )

  ditto.updateTransportConfig((config) => {
    config.connect.websocketURLs.add('wss://...')
  })
  ditto.disableSyncWithV3()
  await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
  ditto.startSync()

  // Presence
  ditto.observePeers((peers) => {
    peers.forEach((peer) => {
      console.log(peer.peerKeyString, peer.isConnectedToDittoCloud)
    })
  })

  // Cleanup
  ditto.stopSync()
  await ditto.close()
  ```

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

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

  const ditto = await Ditto.open(config)

  await ditto.auth.setExpirationHandler(async (ditto, timeUntilExpiration) => {
    try {
      const result = await ditto.auth.login('your-token', Authenticator.DEVELOPMENT_PROVIDER)
      if (result.error) {
        console.error(`Auth failed: ${result.error}, time until expiration: ${timeUntilExpiration}`)
      }
    } catch (error) {
      console.error(`Auth exception: ${error}`)
    }
  })

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

  ditto.sync.start()

  // Store observer (replaces live query)
  const observer = ditto.store.registerObserver('SELECT * FROM cars', (result) => {
    updateUI(result.items)
  })

  // Sync subscription (replaces collection subscription)
  const subscription = ditto.sync.registerSubscription('SELECT * FROM cars')

  // Presence
  ditto.presence.observe((graph) => {
    const allPeers = [graph.localPeer, ...graph.remotePeers]
    allPeers.forEach((peer) => {
      console.log(peer.peerKey, peer.isConnectedToDittoServer)
    })
  })

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

  CHECKLIST:
  - Replace `new Ditto(identity, path)` with `await Ditto.open(new DittoConfig(...))`
  - Add `await` for initialization — `Ditto.open()` is async
  - Create `DittoConfig` with `databaseID` and connect mode (`server` or `smallPeersOnly`)
  - Set `DQL_STRICT_MODE=true` BEFORE `sync.start()` if maintaining v4 strict behavior
  - Set up `auth.setExpirationHandler()` and use `auth.login()` for authentication
  - Replace `ditto.startSync()` / `stopSync()` with `ditto.sync.start()` / `stop()`
  - Replace `ditto.isSyncActive` with `ditto.sync.isActive`
  - Replace all `store.collection('x')` calls with `store.execute()` DQL queries
  - Use parameterized DQL queries (`:paramName`) — never string interpolation
  - Replace `collection.upsert()` with `INSERT INTO ... DOCUMENTS (:doc)`
  - Replace `collection.findByID(id).update()` with `UPDATE ... SET ... WHERE _id = :id`
  - Replace `collection.findByID(id).remove()` with `DELETE FROM ... WHERE _id = :id`
  - Replace `collection.findAll().observeLocal(handler)` with `store.registerObserver(query, handler)`
  - Note: v5 registerObserver parameter order is (query, handler, args?) — handler before args
  - Replace `liveQuery.stop()` with `observer.cancel()`
  - Replace `collection.findAll().subscribe()` with `sync.registerSubscription(query)`
  - Replace `store.write()` with `store.transaction()`
  - Replace `auth.loginWithToken()` with `auth.login()`
  - Remove `AuthenticationHandler` interface usage
  - Remove `disableSyncWithV3()` calls
  - Remove `updateTransportConfig` workaround patterns for websocket URLs (method still exists for advanced use)
  - Update `peer.peerKeyString` → `peer.peerKey`
  - Update `peer.isConnectedToDittoCloud` → `peer.isConnectedToDittoServer`
  - Update `connection.peerKeyString1`/`peerKeyString2` → `connection.peer1`/`peer2`
  - Update `connectionRequest.peerKeyString` → `connectionRequest.peerKey`
  - Replace `ditto.observePeers()` with `ditto.presence.observe()`
  - Update `AttachmentToken` class usage to plain `{ id, len, metadata }` objects
  - 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`
  - Remove generic type parameters from `StoreObserver<T, U>` and `SyncSubscription<T>`

  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'
  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}
  // Everything mixed together
  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();
  ```
</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

**Initialization Migration Steps**

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

    <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);
      ditto.sync.start();
      ```

      ```typescript JAVASCRIPT (v4) theme={null}
      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 [Node.js Legacy→DQL Migration Guide](./node-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)` — no longer accepts `AttachmentToken` instances |
| `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` export                         | Removed — use `ASC`/`DESC` in DQL `ORDER BY`                                          |
| `WriteStrategy` export                         | Removed — use `ON ID CONFLICT DO ...` in DQL                                          |
| `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' } }
    );
});
```

### Attachment Token Change

```typescript theme={null}
// v4 — AttachmentToken instance
const token: AttachmentToken = doc.attachment;
const result = await ditto.store.fetchAttachment(token, (event) => { });

// v5 — plain object from query result
const token = item.value['photo']; // plain object, not AttachmentToken
const result = await ditto.store.fetchAttachment(token, (event) => { });
```

### Observer Type Parameter

```typescript theme={null}
// v4 — two type params
const observer = ditto.store.registerObserver<Car, CarExtra>(query, {}, handler);

// v5 — one type param, handler before args
const observer = ditto.store.registerObserver<Car>(query, handler, {});
```

<Note>
  **Parameter order changed**: In v5, `registerObserver` takes `(query, handler, queryArguments?)` — the handler comes before the query arguments.
</Note>

### DQL Query Arguments (adds bigint support)

```typescript theme={null}
// v4
import { QueryArguments } from '@dittolive/ditto';
const args: QueryArguments = { color: 'blue', count: 42 };

// v5 — renamed, adds bigint
import { DQLQueryArguments } from '@dittolive/ditto';
const args: DQLQueryArguments = { color: 'blue', count: 42n }; // bigint supported
```

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

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

***

## 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` / `PendingIDSpecificOperation`       | 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 `ORDER BY ASC/DESC` and `ON ID CONFLICT DO ...` |
| `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                                                 |
| `TransportConfigListenHTTP.staticContentPath`                                | 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
* **`DittoConfig.DEFAULT_DATABASE_ID`** / **`.default`** / **`.copy()`** — config helpers
* **`{ 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[]` (replaces removed `mutatedDocumentIDs()`)

***

## Migration Checklist

### Initialization

* [ ] 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
* [ ] 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` on `Ditto.open()`**: `Ditto.open()` returns a Promise. Forgetting `await` means you're working with a Promise, not a Ditto instance.

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

4. **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.

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

6. **Observer parameter order**: v5 `registerObserver(query, handler, args?)` puts the handler before the query arguments — different from v4 where args came before handler.

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

***
