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

Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:
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();
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
Web-specific: await init() is still required before any Ditto API calls. It initializes the WebAssembly module. This is unchanged from v4.
Initialization Migration Steps
1

Replace Initialization

Replace new Ditto() constructor with await Ditto.open(config).
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();
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 JavaScript 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)
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 / WriteStrategy exportsRemoved β€” use DQL equivalents
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' } }
    );
});

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.
Parameter order: Both registerObserver and registerObserverWithSignalNext take (query, handler, queryArguments?) β€” the handler comes before the query arguments.
// 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 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 / PendingCursorOperationUse 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 equivalents
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
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
  • { 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.