Skip to main content

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.

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

Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:
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();
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
1

Replace Initialization

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

const ditto = await Ditto.open(config);
ditto.sync.start();
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
2

Update Authentication

Set authentication handler separately after initialization.
// 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}`);
        }
});
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

Additional Changes

DQL Strict Mode Behavior Change

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.
Choose the appropriate migration path based on your current v4 configuration:
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.
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.
To migrate to DQL_STRICT_MODE=false (the new v5 default), contact Ditto Customer Support for guidance.
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();
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.
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();
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:
    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();
    
  2. Convert all queries from Legacy Query Builder to DQL See the Node.js Legacy→DQL Migration Guide for detailed conversion examples.
  3. Upgrade to v5 No DQL configuration changes requiredβ€”v5 defaults to DQL_STRICT_MODE=false.
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:
const config = new DittoConfig(
    "your-database-id",
    {
        mode: 'server',
        url: "https://your-server.ditto.live"
    },
    "/app/data/ditto"  // Old v4 path
);

API Renames

v4.14v5.0
new Ditto(identity) constructorDitto.open(config) (async) or Ditto.openSync(config)
ditto.startSync()ditto.sync.start()
ditto.stopSync()ditto.sync.stop()
ditto.isSyncActiveditto.sync.isActive
ditto.appIDditto.config.databaseID
ditto.siteIDRemoved
ditto.sdkVersionDitto.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 typeDQLQueryArguments (adds bigint support)
SortDirection exportRemoved β€” use ASC/DESC in DQL ORDER BY
WriteStrategy exportRemoved β€” use ON ID CONFLICT DO ... in DQL
peerKeyStringpeerKey
isConnectedToDittoCloudisConnectedToDittoServer
connection.peerKeyString1 / peerKeyString2connection.peer1 / peer2
connection.approximateDistanceInMetersREMOVED β€” no replacement

Store.write() β†’ Store.transaction()

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

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

// 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, {});
Parameter order changed: In v5, registerObserver takes (query, handler, queryArguments?) β€” the handler comes before the query arguments.

DQL Query Arguments (adds bigint support)

// 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.
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.
// 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 APINotes
new Ditto() constructorUse Ditto.open(config) or Ditto.openSync(config)
ditto.disableSyncWithV3()v3 protocol support fully dropped
ditto.runGarbageCollection()GC is now automatic
ditto.siteIDRemoved entirely
ditto.observePeers()Use ditto.presence.observe()
Document / MutableDocumentDQL returns plain objects
DocumentID / DocumentPath / MutableDocumentPathRemoved with legacy collection API
Counter / Register CRDT typesUse DQL PN_INCREMENT for counters
Collection / PendingCursorOperation / PendingIDSpecificOperationUse store.execute() with DQL
LiveQuery / LiveQueryEventUse store.registerObserver()
WriteTransaction / WriteTransactionCollection / WriteTransactionResultUse store.transaction() with DQL
UpdateResult / UpdateResultsMapDQL returns row counts
PresenceManagerUse ditto.presence directly
SortDirection / WriteStrategyUse DQL ORDER BY ASC/DESC and ON ID CONFLICT DO ...
AttachmentToken classUse plain { id, len, metadata } objects
Address / addressToString()Removed entirely
auth.loginWithToken() / auth.loginWithUsernameAndPassword()Use auth.login()
Logger.setLogFile() / Logger.setLogFileURL()Removed
Logger.emojiLogLevelHeadingsEnabledRemoved
TransportConfigListenHTTP.staticContentPathRemoved
connection.approximateDistanceInMetersRemoved

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.