Skip to main content

Overview

This guide covers the essential changes needed to migrate your Ditto Rust app from v4 to v5. The main architectural shift is moving from identity/builder-based initialization to a four-phase configuration model with async APIs and Result-based error handling. 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 Rust 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
  • Simplified initialization with DittoConfig::new() replacing the builder pattern
  • Struct-based expiration handler for type-safe, re-callable authentication
  • No workarounds needed β€” v5 auto-configures the websocket URL from the Server connect URL, and removes DQL strict mode and v3 sync workarounds
Initialization Migration Steps
1

Replace Initialization

Replace Ditto::builder()...build() with Ditto::open(config).
Key changes:
  • Use DittoConfig::new(database_id, connect) instead of DittoBuilder
  • AppId β†’ DatabaseId (or database_id: String in config)
  • OnlinePlayground β†’ DittoConfigConnect::Server { url: Url }
  • OfflinePlayground β†’ DittoConfigConnect::SmallPeersOnly { private_key: None }
  • SharedKey β†’ DittoConfigConnect::SmallPeersOnly { private_key: Some(key_bytes) }
  • URL fields use url::Url type β€” parse from string with .parse().unwrap()
  • Ditto::open() is async, Ditto::open_sync() is the blocking alternative
  • Remove update_transport_config calls β€” v5 automatically configures the websocket URL from the DittoConfigConnect::Server URL
  • Remove disable_sync_with_v3() calls and DQL strict mode workaround queries
2

Update Authentication

Set authentication handler separately after initialization.
Async closure limitation: set_expiration_handler requires a handler callable multiple times (AsyncFn). An async move closure that captures variables from its environment only implements AsyncFnOnce (consumed on first call), so it will not compile. A bare async || without move captures by reference and won’t satisfy the 'static bound.Use a struct-based handler whenever your handler captures state (tokens, config, etc.) β€” which is virtually every real application.
Key changes:
  • ditto.auth() returns Option<DittoAuthenticator> β€” unwrap with .expect() in Server mode
  • Implement DittoAuthExpirationHandler on a struct to hold captured state (tokens, config)
  • on_expiration takes &self β€” the struct persists across calls; .clone() state into each future
  • Call ditto.auth() synchronously before the async move block to avoid lifetime issues with borrowing ditto
  • auth.login() is synchronous β€” returns Result<AuthenticationClientFeedback, DittoError> directly
  • Use get_development_provider() function for playground tokens
  • Custom auth: pass your provider name as &str to login()

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 Rust 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-{database_id} instead of ditto To maintain v4 compatibility:

API Renames

AppId β†’ DatabaseId

Update all AppId type references and variable names throughout your codebase.

_v2 Suffix Removed

All _v2 method variants are now the canonical API β€” the suffix is dropped:
Note: v5 store.execute() uses the IntoQuery trait. Pass a string for queries without args, or a tuple (query, args) for parameterized queries where args implements serde::Serialize.

Back Pressure (register_observer_with_signal_next)

The standard register_observer automatically calls signal_next() after your handler returns, and callbacks are never concurrent β€” one callback must return before the next fires. For handlers that do expensive work before they’re ready for the next update, use register_observer_with_signal_next to call signal_next when you choose.
Coalescing behavior: While your handler is processing, Ditto coalesces intermediate changes. When you call signal_next(), you receive a single callback with the latest state β€” not every intermediate change.

APIs Removed Without Replacement


New APIs in v5

  • Ditto::open(DittoConfig) β€” async factory replacing DittoBuilder
  • Ditto::open_sync(DittoConfig) β€” synchronous variant
  • DatabaseId β€” replaces AppId
  • DittoConfig::new(database_id, connect) β€” unified config with system_parameters support
  • DittoConfigConnect::Server { url: Url } / DittoConfigConnect::SmallPeersOnly { private_key: Option<Vec<u8>> } β€” connect mode variants
  • ditto.sync().start() / .stop() / .is_active() β€” sync lifecycle on Sync object
  • ditto.version() β€” static method, replaces Ditto::with_sdk_version()
  • DiskUsageItem β€” replaces DiskUsageChild
  • DittoLogger::set_custom_log_callback() β€” custom log callback support
  • get_development_provider() β€” returns the development auth provider name
  • store.execute() and store.register_observer() as canonical DQL API (no more _v2 suffix)

Migration Checklist

Initialization

  • Replace Ditto::builder()...build() with Ditto::open(config).await?
  • Create DittoConfig::new(database_id, connect) with appropriate connect mode
  • Update OnlinePlayground β†’ DittoConfigConnect::Server { url: "...".parse().unwrap() }
  • Update OfflinePlayground β†’ DittoConfigConnect::SmallPeersOnly { private_key: None }
  • Update SharedKey β†’ DittoConfigConnect::SmallPeersOnly { private_key: Some(key_bytes) }
  • Remove PersistentRoot / with_root() setup
  • Remove update_transport_config calls
  • Remove disable_sync_with_v3() calls
  • Set DQL_STRICT_MODE=true BEFORE starting sync if maintaining v4 behavior

Authentication

  • Get authenticator: let auth = ditto.auth().expect("Auth available in Server mode")
  • Implement DittoAuthExpirationHandler on a struct to hold captured state (do NOT use async move closures with captured variables)
  • auth.login() is synchronous β€” returns Result<AuthenticationClientFeedback, DittoError>
  • Use get_development_provider() for playground tokens
  • Remove authentication from identity configuration

Sync

  • Update ditto.start_sync() β†’ ditto.sync().start()
  • Update ditto.stop_sync() β†’ ditto.sync().stop()
  • Update ditto.is_sync_active() β†’ ditto.sync().is_active()

Data Operations

  • Replace all execute_v2() calls with execute()
  • Replace all register_observer_v2() calls with register_observer()
  • Replace all register_observer_with_signal_next_v2() with register_observer_with_signal_next()
  • Use parameterized queries β€” never string formatting
  • Replace collection-based operations with DQL via store().execute()

Breaking Changes

  • Set with_persistence_directory() if maintaining v4 directory structure
  • Update peer_key_string β†’ peer_key: String (unified, no more Vec<u8> variant)
  • Update peer_key_string1/2 β†’ peer1/2
  • Update is_connected_to_ditto_cloud β†’ is_connected_to_ditto_server
  • Access store via ditto.store() method (parentheses required)
  • Replace AppId β†’ DatabaseId (type rename throughout)
  • Replace DiskUsageChild β†’ DiskUsageItem
  • Replace Ditto::with_sdk_version(|v| {...}) β†’ Ditto::version()
  • Remove Connection.approximate_distance_in_meters usage

Verification

  • Build compiles with zero errors (cargo build)
  • No deprecated API warnings
  • Observers update data immediately
  • Authentication works before sync starts
  • No memory leaks

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. ditto.auth() returns Option: In Server mode, auth() returns Some(authenticator). In SmallPeersOnly mode, it returns None. Always .expect() or match when you know you’re in Server mode.
  3. auth.login() is synchronous: Unlike other SDKs, the Rust login() returns Result directly β€” no callback or async. But the expiration handler itself IS async.
  4. URL type for Server connect: DittoConfigConnect::Server { url } uses url::Url, not String. Parse with "https://...".parse().unwrap().
  5. String interpolation in queries: Never format!("SELECT * FROM cars WHERE color = '{}'", color). Use the IntoQuery trait: store().execute(("SELECT * FROM cars WHERE color = :color", json!({"color": color}))).await?.
  6. 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.
  7. Missing ON ID CONFLICT: INSERT fails if document _id already exists without a conflict clause.
  8. async move closures in set_expiration_handler: The handler must be callable multiple times (AsyncFn), but async move closures that capture variables only implement AsyncFnOnce. Implement DittoAuthExpirationHandler on a struct instead β€” see the Authentication section above.