Skip to main content

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.

Ditto Instance Initialization

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

API Renames

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

Attachment Token Change

Observer Type Parameter

Parameter order changed: In v5, registerObserver takes (query, handler, queryArguments?) β€” the handler comes before the query arguments.

DQL Query Arguments (adds bigint support)

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.

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