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 Flutter app from v4 to v5. The main architectural shift is moving from identity-based initialization to DittoConfig, plus several breaking type changes. 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 Flutter app from v4 to v5.
I need help migrating a Ditto Flutter application from v4 to v5. Focus on these critical changes:

INITIALIZATION:
Replace `Ditto.open(identity: Identity)` with `Ditto.open(DittoConfig(...))`:
- `DittoConfig(databaseID: String, connect: DittoConfigConnect, ...)` replaces identity types
- `DittoConfigConnectServer(url: String)` replaces `OnlinePlaygroundIdentity` β€” URL is a plain `String`, NOT a `Uri`
- `DittoConfigConnectSmallPeersOnly(privateKey: String?)` replaces `OfflinePlaygroundIdentity` and `SharedKeyIdentity`
- `Ditto.openSync(config)` available as synchronous alternative
- `await Ditto.init()` must be called before any Ditto usage
- Remove `updateTransportConfig` WebSocket URL additions (cloud URL inferred from config)
- Remove `DQL_STRICT_MODE = false` workaround (now the v5 default)
- If your v4 app relied on `DQL_STRICT_MODE = true` (v4 default), set it BEFORE `sync.start()`:
  `await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = true")`

SYNC:
- `ditto.startSync()` β†’ `ditto.sync.start()`
- `ditto.stopSync()` β†’ `ditto.sync.stop()`
- `ditto.isSyncActive` β†’ `ditto.sync.isActive`

AUTHENTICATION:
Set `expirationHandler` via async method after initialization β€” replaces `AuthenticationHandler` interface:
- `await ditto.auth.setExpirationHandler((Ditto ditto, Duration timeUntilExpiration) { ... })`
- `ditto.auth` is non-nullable β€” do NOT use `ditto.auth?.`
- Handler receives `Ditto` (not `Authenticator`) and `Duration` (not `int`)
- Called when `timeUntilExpiration == Duration.zero` (initial auth or expired) or `> 0` (token expiring)
- Use `Authenticator.developmentProvider` for playground tokens
- `ditto.auth.login(token: '...', provider: '...')` β€” returns `Future<AuthResponse>`
- Remove `loginWithCredentials()` β€” use token-based `login()` instead
- Remove `AuthenticationHandler` class/interface

TRANSPORT CONFIG:
Changed from mutable to immutable with builder pattern:
- `ditto.updateTransportConfig((builder) { builder.setAllPeerToPeerEnabled(true); })`
- Or: `ditto.transportConfig = ditto.transportConfig.withAllPeerToPeerEnabled(true);`
- `TransportConfig.withBigPeer(...)` removed β€” use `DittoConfig` server config

PRESENCE & PEERS:
- `PresenceGraph.remotePeers` changed from `List<Peer>` to `Set<Peer>` β€” use `.first` instead of `[0]`
- `Peer.peerKeyString` β†’ `Peer.peerKey: String` (unified)
- `Peer.isConnectedToDittoCloud` β†’ `Peer.isConnectedToDittoServer`
- `Connection.peerKeyString1/peerKeyString2` β†’ `Connection.peer1/peer2`
- `ConnectionRequest.peerKeyString` β†’ `ConnectionRequest.peerKey`

OBSERVERS:
- `registerObserver()` API is the same β€” callback-based with `onChange` parameter
- Observers also expose `Stream<QueryResult> changes` for reactive use
- No `signalNext` β€” backpressure handled internally by SDK

OTHER:
- `ditto.persistenceDirectory` β†’ `ditto.absolutePersistenceDirectory`
- `HttpListenConfig.websocketSync` default changed from `true` to `false`
- `Ditto` and `Store` cannot be passed across Dart isolates
- `DittoLogger.setLogFile()` removed
- `SmallPeerInfoSyncScope` enum removed β€” use DQL `ALTER SYSTEM SET sync_scope`
- `SiteID` and `DocumentID` are no longer exported (internal types)
- Ditto Flutter SDK for MacOS now targets 12.0+ - if your project targets older releases you will need to raise the minimum version.
- Ditto Flutter SDK for iOS now targets 14.0+ - if your project targets older releases you will need to raise the minimum version.
- MacOS and iOS use CocoaPods for dependency management, because of this you will probably need to delete the Pod lock file and run `pod install` again.  This is a standard CocoaPods issue when upgrading dependencies.
- Ditto Flutter SDK for Android minimum SDK version is now 24 (Android 7.0 Nougat). If your project targets `minSdk = 23`, you must raise it.

Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control. Note that Ditto.init() is a prerequisite step that must be called before any Ditto usage:
import 'package:ditto_live/ditto_live.dart';

// 1. Initialize the Ditto system (required before any Ditto usage)
await Ditto.init();

// 2. Configure
final config = DittoConfig(
    databaseID: 'your-database-id',
    connect: DittoConfigConnectServer(
        url: 'https://your-database-id.cloud.ditto.live',
    ),
);

// 3. Open
final ditto = await Ditto.open(config);
// OR synchronous: final ditto = Ditto.openSync(config);

// 4. Authenticate (required for server connections)
await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) {
    ditto.auth.login(
        token: 'your-token',
        provider: Authenticator.developmentProvider,
    );
});

// 5. Start sync
ditto.sync.start();
These changes provide:
  • Clear separation of connection, initialization, and authentication concerns
  • Async initialization prevents blocking the main thread
  • 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 Ditto.open(identity:) with Ditto.open(DittoConfig).
final config = DittoConfig(
    databaseID: 'your-database-id',
    connect: DittoConfigConnectServer(
        url: 'https://your-database-id.cloud.ditto.live',
    ),
);
final ditto = await Ditto.open(config);
ditto.sync.start();
Identity β†’ DittoConfig mapping:
v4.14 Identityv5.0 DittoConfig connect
OnlinePlaygroundIdentity(appID:token:customAuthUrl:)DittoConfigConnectServer(url:)
OnlineWithAuthenticationIdentity(appID:customAuthUrl:)DittoConfigConnectServer(url:) + auth handler
OfflinePlaygroundIdentity(appID:siteID:)DittoConfigConnectSmallPeersOnly(privateKey: null)
SharedKeyIdentity(appID:sharedKey:siteID:)DittoConfigConnectSmallPeersOnly(privateKey: key)
Key changes:
  • Use DittoConfig with databaseID instead of identity types
  • appID β†’ databaseID
  • Connect mode selected via DittoConfigConnect* subclasses
  • DittoConfigConnectServer.url takes a String, not a Uri
  • New: Ditto.openSync(config) synchronous alternative
  • ditto.sync.start() instead of ditto.startSync()
2

Update Authentication

Set authentication handler after initialization.
// Set handler after Ditto.open() β€” note: setExpirationHandler is async
await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) {
    // Called for initial auth (timeUntilExpiration == Duration.zero)
    // and when token is expiring
    ditto.auth.login(
        token: 'your-token',
        provider: Authenticator.developmentProvider,
    );
});
Key changes:
  • AuthenticationHandler interface removed β€” use AuthenticationExpirationHandler typedef
  • Set handler via await ditto.auth.setExpirationHandler(...) (async method, not property assignment)
  • ditto.auth is non-nullable in v5 (no ?. needed)
  • Handler signature: void Function(Ditto ditto, Duration timeUntilExpiration) β€” receives Ditto instance and Duration, not Authenticator and int
  • Authenticator.loginWithCredentials(username:password:provider:) removed β€” use token-based login(token:provider:)
  • Authenticator.developmentProvider is the dev provider constant

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.
final config = DittoConfig(
    databaseID: 'your-database-id',
    connect: DittoConfigConnectServer(
        url: 'https://your-database-id.cloud.ditto.live',
    ),
);
final 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.
final config = DittoConfig(
    databaseID: 'your-database-id',
    connect: DittoConfigConnectServer(
        url: 'https://your-database-id.cloud.ditto.live',
    ),
);
final 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:
    final ditto = await Ditto.open(
        identity: OnlinePlaygroundIdentity(
            appID: 'your-app-id',
            token: 'your-token',
            enableDittoCloudSync: false,
            customAuthUrl: 'https://your-app-id.cloud.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 Flutter 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. This is for only new databases created in v5. v5 does not migrate existing databases to the new structure. Provide custom persistence directory:
final config = DittoConfig(
    databaseID: 'your-database-id',
    connect: DittoConfigConnectServer(
        url: 'https://your-database-id.cloud.ditto.live',
    ),
    persistenceDirectory: '/app/data/ditto',  // custom path to preserve v4 data
);

Observer Changes

v4 and v5 both use callback-based observers. The core registerObserver API pattern is the same in both versions, returning a StoreObserver that you retain and call .cancel() on for cleanup.
final observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    arguments: {"color": "red"},
    onChange: (result) {
        setState(() { _cars = result.items; });
    },
);

// Cleanup
observer.cancel();

// Alternative: use the Stream-based API
final observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    arguments: {"color": "red"},
);
observer.changes.listen((result) {
    setState(() { _cars = result.items; });
});
Key changes:
  • The observer lifecycle pattern (retain, .cancel(), cleanup) is the same as v4
  • v5 observers expose a Stream<QueryResult> changes property for reactive use
  • StoreObserver.queryString and queryArguments properties available for introspection

Back Pressure

The Flutter SDK handles back pressure internally through its Stream<QueryResult>-based observer model. There is no signalNext function to call β€” the SDK manages update delivery automatically.
// Observers use Stream internally β€” backpressure is handled by the SDK
final observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    arguments: {"color": "blue"},
    onChange: (result) {
        setState(() { _cars = result.items; });
    },
);
If you need fine-grained flow control, use a Dart StreamController with pause / resume on the observer’s stream.

PresenceGraph.remotePeers Type Changed

Breaking Change: PresenceGraph.remotePeers changed type from List<Peer> to Set<Peer>.
// v4 β€” List, index access works
final first = graph.remotePeers[0];

// v5 β€” Set, use .first
final first = graph.remotePeers.first;
// Or: graph.remotePeers.toList()[0]

WebSocket Sync Default Changed

HttpListenConfig.websocketSync now defaults to false (was true in v4). If your app relied on WebSocket sync being enabled automatically, you must now enable it explicitly.

TransportConfig Now Immutable

TransportConfig and all its sub-types are now @immutable. Use the builder pattern or the updateTransportConfig convenience method:
// v5 β€” Using updateTransportConfig (recommended)
ditto.updateTransportConfig((builder) {
    builder.setAllPeerToPeerEnabled(true);
});

// v5 β€” Using builder pattern directly
final newConfig = ditto.transportConfig.toBuilder()
    ..setAllPeerToPeerEnabled(true);
ditto.transportConfig = newConfig.build();

// v5 β€” Using immutable copy method
ditto.transportConfig = ditto.transportConfig.withAllPeerToPeerEnabled(true);
// v4 β€” TransportConfig was mutable
ditto.updateTransportConfig((config) {
    config.setAllPeerToPeerEnabled(true);
    config.connect.webSocketUrls.add('wss://...');
});

Isolate Restriction

Ditto and Store are marked @pragma("vm:isolate-unsendable"). They cannot be passed across Dart isolates.

API Renames

v4.14v5.0
ditto.startSync()ditto.sync.start()
ditto.stopSync()ditto.sync.stop()
ditto.isSyncActiveditto.sync.isActive
Peer.peerKeyString + Peer.peerKey: Uint8ListPeer.peerKey: String (unified)
Peer.isConnectedToDittoCloudPeer.isConnectedToDittoServer
Connection.peerKeyString1 / peerKeyString2Connection.peer1 / Connection.peer2
ConnectionRequest.peerKeyStringConnectionRequest.peerKey
ditto.persistenceDirectoryditto.absolutePersistenceDirectory
SmallPeerInfo.syncScope: SmallPeerInfoSyncScopeDQL: ALTER SYSTEM SET sync_scope = '...'
SmallPeerInfoSyncScope enumRemoved β€” use DQL
AuthenticationHandler interfaceAuthenticationExpirationHandler typedef
Authenticator.loginWithCredentials(username:password:provider:)Removed β€” use token-based login(token:provider:)
TransportConfig.withBigPeer(...)Removed β€” use DittoConfig server configuration
DittoLogger.setLogFile(...)Removed β€” file logging no longer supported
HttpListenConfig.staticContentPathRemoved β€” no replacement
PresenceGraph.remotePeers: List<Peer>PresenceGraph.remotePeers: Set<Peer>
SiteID classNot exported (internal type)
DocumentID classNot exported (internal type)

Key Initialization Changes

v4.14v5.0Notes
Ditto.open(identity: Identity)Ditto.open(DittoConfig)Positional parameter, DittoConfig has default
Identity subclassesDittoConfigConnect* subclassesSee identity mapping table
OnlinePlaygroundIdentity.customAuthUrl: String?DittoConfigConnectServer(url: String)URL is now a String, not Uri
OnlinePlaygroundIdentity.enableDittoCloudSyncRemovedCloud sync is configured via connect mode

Authentication API Changes

v4.14v5.0Notes
AuthenticationHandler interfaceAuthenticationExpirationHandler typedefvoid Function(Ditto, Duration)
authenticationRequired(Authenticator)expirationHandler called with Duration.zeroSingle handler replaces two callbacks
authenticationExpiringSoon(Authenticator, int)expirationHandler called with remaining DurationCombined into one handler
authenticator.loginWithCredentials(username:password:provider:)RemovedUse login(token:provider:)
authenticator.login(token:provider:)ditto.auth.login(token:provider:)Same API, accessed from Ditto.auth
Set handler via identity constructorawait ditto.auth.setExpirationHandler(...)Async method, set after Ditto.open()

APIs Removed Without Replacement

Removed APINotes
Connection.approximateDistanceInMetersPeer distance estimation removed
DittoLogger.setLogFile(...)File logging removed entirely
HttpListenConfig.staticContentPathStatic HTTP file serving removed
TransportConfig.withBigPeer(...)Use DittoConfig server config instead
SmallPeerInfoSyncScope enumSync scope is now a DQL ALTER SYSTEM setting
ManualIdentityManual certificate identity removed

New APIs in v5

  • Ditto.init() β€” required static initialization before using any Ditto features (throws if not called)
  • DittoConfig / DittoConfigConnectServer / DittoConfigConnectSmallPeersOnly β€” new configuration hierarchy replacing all identity types
  • Ditto.openSync(config) β€” synchronous constructor alternative to async Ditto.open(config)
  • Ditto.defaultRootDirectory β€” static helper for default persistence path
  • ditto.config β€” access the config object used to open the instance
  • AuthenticationExpirationHandler typedef β€” replaces AuthenticationHandler interface
  • Authenticator.developmentProvider β€” constant for development auth provider
  • Authenticator.setExpirationHandler() β€” async method to set the expiration handler
  • SyncSubscription.queryArgumentsCborBytes / queryArgumentsJsonString β€” inspect subscription query arguments
  • Builder types for all transport config sub-types (TransportConfigBuilder, PeerToPeerBuilder, etc.)
  • TransportConfig.withAllPeerToPeerEnabled(bool) β€” immutable copy method for enabling P2P transports
  • Ditto and Store marked @pragma("vm:isolate-unsendable") for safety

Migration Checklist

Initialization

  • Ensure await Ditto.init() is called before any Ditto usage
  • Replace Ditto.open(identity:) with Ditto.open(DittoConfig(...))
  • Create DittoConfig with databaseID and connect mode
  • Update identity types to DittoConfigConnect* variants
  • Use String URLs (not Uri) for DittoConfigConnectServer
  • Remove updateTransportConfig calls for WebSocket URLs (cloud URL inferred from config)
  • Set DQL_STRICT_MODE=true BEFORE sync.start() if maintaining v4 behavior
  • Update startSync() β†’ sync.start()

Authentication

  • Replace AuthenticationHandler interface with AuthenticationExpirationHandler typedef
  • Set handler via await ditto.auth.setExpirationHandler(...) after Ditto.open()
  • Update handler signature: receives (Ditto ditto, Duration timeUntilExpiration), not (Authenticator, int)
  • Replace loginWithCredentials(username:password:provider:) with login(token:provider:)
  • Use Authenticator.developmentProvider for dev tokens
  • Remove ?. on ditto.auth β€” it is non-nullable in v5

Breaking Changes

  • Update remotePeers[0] β†’ remotePeers.first (type changed from List to Set)
  • Check HttpListenConfig.websocketSync β€” must be explicitly enabled now (default is false)
  • Update TransportConfig usage to builder pattern: .toBuilder()...build() or updateTransportConfig()
  • Update peerKeyString β†’ peerKey
  • Update isConnectedToDittoCloud β†’ isConnectedToDittoServer
  • Update persistenceDirectory β†’ absolutePersistenceDirectory
  • Do not pass Ditto or Store across isolates
  • Set persistenceDirectory in DittoConfig if maintaining v4 directory structure

Verification

  • Build compiles with zero errors
  • Observers update data immediately
  • Authentication works before sync starts
  • No remotePeers index access errors
  • WebSocket sync works if needed (explicitly enabled)