Migration

    Couchbase Lite 4.0.0 introduces several important updates and API changes to simplify development and ensure consistency across all platforms. Most APIs deprecated in versions 3.1.0 through 3.3.x have now been removed, and several configurations have been updated or restructured.

    This guide provides step-by-step instructions for upgrading from Couchbase Lite 3.2.x / 3.3.x to 4.0.0. Follow these guidelines to ensure a smooth migration with minimal disruption to your applications.

    Document Operations on the Default Collection

    Starting in Couchbase Lite 3.1.0, scope and collections were introduced to better organize documents within a database. At that time, document operations performed directly through the Database object for the default collection, such as get, save, delete, purge, count, set or get expiration, index management, and listeners, were deprecated.

    In 4.0.0, all deprecated document APIs in the Database class have been removed. To work with documents in the default collection, retrieve the default collection from the database and use the collection’s methods instead.

    • Swift

    • Objective-C

    • Java

    • C#

    • C

    // Before
    database.save(document)
    
    // After
    let collection = try database.defaultCollection()
    try collection.save(document: document)
    // Before
    [database saveDocument: document error: &error];
    
    // After
    CBLCollection* collection = [database defaultCollection: &error];
    [collection saveDocument: document error: &error];
    // Before
    database.save(document);
    
    // After
    Collection collection = database.getDefaultCollection();
    collection.save(document);
    // Before
    database.Save(document);
    
    // After
    var collection = database.GetDefaultCollection();
    collection.Save(document);
    // Before
    CBLDatabase_SaveDocument(database, document, &error);
    
    // After
    CBLCollection* collection = CBLDatabase_DefaultCollection(database, &error);
    CBLCollection_SaveDocument(collection, document, &error);

    ReplicatorConfiguration and CollectionConfiguration

    When collections were introduced in 3.1.0, the ReplicatorConfiguration constructor that accepted a Database for the default collection was deprecated. A new API was introduced to let developers explicitly add collections to the ReplicatorConfiguration, each with an optional CollectionConfiguration as needed.

    In 4.0.0, the old constructor and related APIs have been removed. Collections must now be specified up front when creating a ReplicatorConfiguration. They can no longer be added or removed dynamically. This ensures consistency and prevents accidental inclusion of the default collection, which previously caused confusion or replication errors when not present on Sync Gateway.

    • Swift

    • Objective-C

    • Java

    • C

    // Before
    let config = ReplicatorConfiguration(database, target: endpoint)
    
    // After
    let collections = [defaultCollection].map { CollectionConfiguration(collection: $0) }
    let config = ReplicatorConfiguration(collections: collections, target: endpoint)
    // Before (with database)
    CBLReplicatorConfiguration* config =
        [[CBLReplicatorConfiguration alloc] initWithDatabase: database
                                                       target: endpoint];
    
    // After
    CBLCollectionConfiguration* colConfig =
     [[CBLCollectionConfiguration alloc] initWithCollection: defaultCollection];
    CBLReplicatorConfiguration* config =
        [[CBLReplicatorConfiguration alloc] initWithCollections: @[colConfig]
                                                          target: endpoint];
    // Before
    ReplicatorConfiguration config = new ReplicatorConfiguration(database, endpoint);
    
    // After
    CollectionConfiguration colConfig = new CollectionConfiguration(defaultCollection);
    ReplicatorConfiguration config = new ReplicatorConfiguration(Set.of(colConfig), endpoint);
    // Before
    CBLReplicatorConfiguration config = {
        .database = database,
        .endpoint = endpoint,
        // ... other fields
    };
    
    // After
    CBLCollectionConfiguration colConfig = {
        .collection = defaultCollection
    };
    CBLReplicatorConfiguration config = {
        .collections = &colConfig,
        .collectionCount = 1,
        .endpoint = endpoint,
        // ... other fields
    };

    CBLReplicatorConfiguration Changes in Couchbase Lite C

    Couchbase Lite C uses a struct-based configuration and has never supported dynamic collection management. In version 4.0.0, several updates were made to align the API with other platforms:

    • The deprecated database property for the default collection in CBLReplicatorConfiguration has been removed.

    • The configuration fields have been reordered so that the required fields—collections, collectionCount, and endpoint—appear first, followed by commonly used fields such as replicatorType, authenticator, context, and pinnedServerCertificate.

    • CBLReplicatorCollection is now defined as an alias of CBLCollectionConfiguration. Developers are encouraged to use CBLCollectionConfiguration going forward, as CBLReplicatorCollection is not deprecated but will be removed in the next major release.

    Developers who create configurations using positional initialization should review and update the field order according to the latest ReplicatorConfiguration documentation.

    Furthermore, the deprecated propertyEncryptor and propertyDecryptor callbacks, previously used for encrypting and decrypting document properties in the default collection, have been removed. Developers should use the new documentPropertyEncryptor and documentPropertyDecryptor callbacks instead, which provide consistent and extensible encryption support across collections.

    .NET Configuration API

    Couchbase Lite 4.0 modernizes its .NET configuration model to make configuration objects immutable and type-safe, leveraging new C# language features:

    • It uses C# 9 init-only properties and C# 11 required properties to enforce immutability and ensure that all mandatory fields are set at creation time.

    • Constructors now require all required properties, while optional properties can be set only during initialization.

    • The required properties are also declared as init-only to allow modifications when cloning an existing configuration using the C# 'with' keyword.

    This design ensures configuration objects remain predictable, reusable, and safe from unintended changes.

    // Before
    var config = new ReplicatorConfiguration(endpoint);
    config.AddCollection(DefaultCollection);
    config.ReplicatorType = ReplicatorType.Push;
    
    // Copy
    var updated = new ReplicatorConfiguration(config);
    updated.ReplicatorType = ReplicatorType.Pull;
    
    // After
    var collections = CollectionConfiguration.FromCollections(DefaultCollection);
    var config = new ReplicatorConfiguration(collections, endpoint) {
        ReplicatorType = ReplicatorType.Push
    };
    
    // Copy immutably using 'with'
    var updated = config with { ReplicatorType = ReplicatorType.Pull };

    Replication Pending Documents

    The replication pending documents API applies only to documents that are waiting to be pushed to the remote endpoint. The deprecated methods for retrieving or checking pending documents without specifying a collection have been removed in 4.0.0. Use the corresponding methods that require a Collection object instead.

    • Swift

    • Objective-C

    • Java

    • C#

    • C

    // Before
    let pendings = try replicator.pendingDocumentIds()
    let isPending = try replicator.isDocumentPending("doc1")
    
    // After
    let defaultCollection = try database.defaultCollection()
    let pendings = try replicator.pendingDocumentIds(collection: defaultCollection)
    let isPending = try replicator.isDocumentPending("doc1", collection: defaultCollection)
    // Before
    NSArray* pendings = [replicator pendingDocumentIds: &error];
    BOOL isPending = [replicator isDocumentPending: @"doc1" error: &error];
    
    // After
    CBLCollection* defaultCollection = [database defaultCollection: &error];
    NSArray* pendings = [replicator pendingDocumentIDsForCollection: defaultCollection
                                                               error: &error];
    BOOL isPending = [replicator isDocumentPending: @"doc1"
                                        collection: defaultCollection
                                             error: &error];
    // Before
    Set<String> pendings = replicator.getPendingDocumentIds();
    boolean isPending = replicator.isDocumentPending("doc1");
    
    // After
    Collection defaultCollection = database.getDefaultCollection();
    Set<String> pendings = replicator.getPendingDocumentIds(defaultCollection);
    boolean isPending = replicator.isDocumentPending("doc1", defaultCollection);
    // Before
    var pendings = replicator.GetPendingDocumentIds();
    var isPending = replicator.IsDocumentPending("doc1");
    
    // After
    var defaultCollection = database.GetDefaultCollection();
    var pendings = replicator.GetPendingDocumentIds(defaultCollection);
    var isPending = replicator.IsDocumentPending("doc1", defaultCollection);
    // Before
    FLDict pendings = CBLReplicator_PendingDocumentIDs(replicator, &error);
    bool isPending = CBLReplicator_IsDocumentPending(replicator, "doc1", &error);
    
    // After
    CBLCollection* defaultCollection = CBLDatabase_DefaultCollection(database, &error);
    FLDict pendings = CBLReplicator_PendingDocumentIDs(replicator, defaultCollection, &error);
    bool isPending = CBLReplicator_IsDocumentPending(replicator, "doc1", defaultCollection, &error);

    Endpoint Listener Configuration

    The deprecated constructors for URLEndpointListenerConfiguration and MessageEndpointListenerConfiguration that accepted a Database object for the default collection have been removed in 4.0.0. Use constructors that accept one or more Collection objects instead.

    • Swift

    • Objective-C

    • Java

    • C#

    • C

    // Before
    let config1 = URLEndpointListenerConfiguration(database: database)
    let config2 = MessageEndpointListenerConfiguration(database: database,
                                                        protocolType: .messageStream)
    
    // After
    let defaultCollection = try database.defaultCollection()
    let config1 = URLEndpointListenerConfiguration(collections: [defaultCollection])
    let config2 = MessageEndpointListenerConfiguration(collections: [defaultCollection],
                                                        protocolType: .messageStream)
    // Before
    CBLURLEndpointListenerConfiguration* config1 =
        [[CBLURLEndpointListenerConfiguration alloc] initWithDatabase: database];
    CBLMessageEndpointListenerConfiguration* config2 =
        [[CBLMessageEndpointListenerConfiguration alloc] initWithDatabase: database
                                                             protocolType: kCBLProtocolTypeMessageStream];
    
    // After
    CBLCollection* defaultCollection = [database defaultCollection: &error];
    CBLURLEndpointListenerConfiguration* config1 =
        [[CBLURLEndpointListenerConfiguration alloc] initWithCollections: @[defaultCollection]];
    CBLMessageEndpointListenerConfiguration* config2 =
        [[CBLMessageEndpointListenerConfiguration alloc] initWithCollections: @[defaultCollection]
                                                                protocolType: kCBLProtocolTypeMessageStream];
    // Before
    URLEndpointListenerConfiguration config1 =
        new URLEndpointListenerConfiguration(database);
    MessageEndpointListenerConfiguration config2 =
        new MessageEndpointListenerConfiguration(database, ProtocolType.MESSAGE_STREAM);
    
    // After
    Collection defaultCollection = database.getDefaultCollection();
    URLEndpointListenerConfiguration config1 =
        new URLEndpointListenerConfiguration(Set.of(defaultCollection));
    MessageEndpointListenerConfiguration config2 =
        new MessageEndpointListenerConfiguration(Set.of(defaultCollection),
                                                  ProtocolType.MESSAGE_STREAM);
    // Before
    var config1 = new URLEndpointListenerConfiguration(database);
    var config2 = new MessageEndpointListenerConfiguration(database, ProtocolType.MessageStream);
    
    // After
    var defaultCollection = database.GetDefaultCollection();
    var config1 = new URLEndpointListenerConfiguration(new[] { defaultCollection });
    var config2 = new MessageEndpointListenerConfiguration(new[] { defaultCollection },
                                                           ProtocolType.MessageStream);
    // Before
    CBLURLEndpointListenerConfiguration config1 = {
        .database = database,
        // ... other fields
    };
    
    // After
    CBLCollection* defaultCollection = CBLDatabase_DefaultCollection(database, &error);
    CBLURLEndpointListenerConfiguration config1 = {
        .collections = &defaultCollection,
        .collectionCount = 1,
        // ... other fields
    };

    TLSIdentity Creation

    Introduced in 3.3.0 for iOS and Android, MultipeerReplicator requires a TLSIdentity that supports both client and server authentication.

    In 4.0.0, the older methods for creating separate client-only or server-only identities have been removed to improve API consistency and prevent confusion. Use the unified API with the KeyUsages flag, which allows you to specify whether the identity is for the client, the server, or both.

    Couchbase Lite C has provided only the unified API since version 3.2.3.

    • Swift

    • Objective-C

    • Java

    • C#

    // Before
    let identity = TLSIdentity.createIdentity(forServer: server,
                                              attributes: certAttrs,
                                              label: "identity1")
    
    // After
    let identity = TLSIdentity.createIdentity(for: .serverAuth,
                                              attributes: certAttrs,
                                              label: "identity1")
    // Before
    CBLTLSIdentity* identity = [CBLTLSIdentity createIdentityForServer: YES
                                                             attributes: certAttrs
                                                                  label: @"identity1"
                                                                  error: &error];
    
    // After
    CBLTLSIdentity* identity = [CBLTLSIdentity createIdentityForKeyUsages: kCBLKeyUsagesServerAuth
                                                                attributes: certAttrs
                                                                     label: @"identity1"
                                                                     error: &error];
    // Before
    TLSIdentity identity = TLSIdentity.createIdentity(true, certAttrs, "identity1");
    
    // After
    TLSIdentity identity = TLSIdentity.createIdentity(Set.of(KeyUsage.SERVER_AUTH), certAttrs, "identity1");
    // Before
    var identity = TLSIdentity.CreateIdentity(true, certAttrs, "identity1");
    
    // After
    var identity = TLSIdentity.CreateIdentity(KeyUsage.ServerAuth, certAttrs, "identity1");

    Logging API

    Since 3.2.2, Couchbase Lite has provided an immutable LogSink API for log configuration. In 4.0.0, the old logging APIs have been removed, leaving LogSink as the single supported log configuration API.

    • Swift

    • Objective-C

    • Java

    • C#

    • C

    // Before
    
    // Console
    Database.log.console.level = .verbose
    
    // File
    var config = LogFileConfiguration(directory: logDir)
    Database.log.file.config = config
    Database.log.file.level = .verbose
    
    // Custom
    Database.log.custom = MyCustomLogger()
    
    // After
    
    // Console
    LogSinks.console = ConsoleLogSink(level: .verbose)
    
    // File
    LogSinks.file = FileLogSink(level: .verbose, directory: logDir)
    
    // Custom
    LogSinks.file = CustomLogSink(level: .verbose, logSink: myCustomLogSink)
    // Before
    
    // Console
    CBLDatabase.log.console.level = kCBLLogLevelVerbose;
    
    // File
    CBLLogFileConfiguration* config = [[CBLLogFileConfiguration alloc] initWithDirectory: logDir];
    CBLDatabase.log.file.config = config;
    CBLDatabase.log.file.level = kCBLLogLevelVerbose;
    
    // Custom
    CBLDatabase.log.custom = myCustomLogger;
    
    // After
    
    // Console
    CBLLogSinks.console = [[CBLConsoleLogSink alloc] initWithLevel: kCBLLogLevelVerbose];
    
    // File
    CBLLogSinks.file = [[CBLFileLogSink alloc] initWithLevel: kCBLLogLevelVerbose
                                                   directory: logDir];
    
    // Custom
    CBLLogSinks.custom = [[CBLCustomLogSink alloc] initWithLevel: kCBLLogLevelVerbose
                                                          logSink: myCustomLogSink];
    // Before
    
    // Console
    Database.log.getConsole().setLevel(LogLevel.VERBOSE);
    
    // File
    LogFileConfiguration config = new LogFileConfiguration(logDir);
    Database.log.getFile().setConfig(config);
    Database.log.getFile().setLevel(LogLevel.VERBOSE);
    
    // Custom
    Database.log.setCustom(new MyCustomLogger());
    
    // After
    
    // Console
    LogSinks.get().setConsole(new ConsoleLogSink(LogLevel.VERBOSE, LogDomain.ALL));
    
    // File
    LogSinks.get().setFile(
        new FileLogSink.Builder()
            .setDirectory(logDir)
            .setLevel(LogLevel.VERBOSE)
            .build()
    );
    
    // Custom
    LogSinks.get().setCustom(new BaseLogSink(LogLevel.VERBOSE) {
        @Override
        protected void writeLog(LogLevel level, LogDomain domain, String message) {
            // Your custom logging implementation
        }
    });
    // Before
    
    // Console
    Database.Log.Console.Level = LogLevel.Verbose;
    
    // File
    var config = new LogFileConfiguration(logDir);
    Database.Log.File.Config = config;
    Database.Log.File.Level = LogLevel.Verbose;
    
    // Custom
    Database.Log.Custom = new MyCustomLogger();
    
    // After
    
    // Console
    LogSinks.Console = new ConsoleLogSink(LogLevel.Verbose);
    
    // File
    LogSinks.File = new FileLogSink(LogLevel.Verbose, logDir);
    
    // Custom
    LogSinks.Custom = new CustomLogSink(LogLevel.Verbose, myCustomLogSink);
    // Before
    
    // Console
    CBLLog_SetConsoleLevel(kCBLLogVerbose);
    
    // File
    CBLLogFileConfiguration config = {
        .level = kCBLLogVerbose,
        .directory = logDir
    };
    CBLLog_SetFileConfig(&config);
    
    // Custom
    CBLLog_SetCallback(myCustomLogCallback);
    
    // After
    
    // Console
    CBLConsoleLogSink* consoleSink = CBLConsoleLogSink_Create(kCBLLogVerbose);
    CBLLogSinks_SetConsole(consoleSink);
    
    // File
    CBLFileLogSink* fileSink = CBLFileLogSink_Create(kCBLLogVerbose, logDir);
    CBLLogSinks_SetFile(fileSink);
    
    // Custom
    CBLCustomLogSink* customSink = CBLCustomLogSink_Create(kCBLLogVerbose, myCustomLogCallback);
    CBLLogSinks_SetCustom(customSink);

    QueryBuilder and Expression Changes

    Several QueryBuilder APIs have been updated or removed in 4.0.0 to improve consistency with Couchbase Server SQL++ syntax.

    DataSource

    The DataSource.database() expression for the default collection has been removed. Use DataSource.collection() instead.

    • Swift

    • Java

    • C#

    // Before
    let query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.database(database))
    
    // After
    let collection = try database.defaultCollection()
    let query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.collection(collection))
    // Before
    Query query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.database(database));
    
    // After
    Collection collection = database.getDefaultCollection();
    Query query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.collection(collection));
    // Before
    var query = QueryBuilder
        .Select(SelectResult.All())
        .From(DataSource.Database(database));
    
    // After
    var collection = database.GetDefaultCollection();
    var query = QueryBuilder
        .Select(SelectResult.All())
        .From(DataSource.Collection(collection));

    Expressions

    The deprecated isNullOrMissing() and notNullOrMissing() functions have been removed. Use isNotValued() and isValued() instead, consistent with Couchbase Server SQL++ syntax.

    • Swift

    • Java

    • C#

    // Before
    let query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.collection(collection))
        .where(Expression.property("name").isNullOrMissing())
    
    // After
    let query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.collection(collection))
        .where(Expression.property("name").isNotValued())
    // Before
    Query query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.collection(collection))
        .where(Expression.property("name").isNullOrMissing());
    
    // After
    Query query = QueryBuilder
        .select(SelectResult.all())
        .from(DataSource.collection(collection))
        .where(Expression.property("name").isNotValued());
    // Before
    var query = QueryBuilder
        .Select(SelectResult.All())
        .From(DataSource.Collection(collection))
        .Where(Expression.Property("name").IsNullOrMissing());
    
    // After
    var query = QueryBuilder
        .Select(SelectResult.All())
        .From(DataSource.Collection(collection))
        .Where(Expression.Property("name").IsNotValued());

    Full-text search APIs have been updated in 4.0.0:

    • FullTextExpression is removed; use FullTextFunction instead.

    • rank() and match() overloads that accept a string index name are removed; use overloads that accept an IndexExpressionProtocol / IndexExpression Interface.

    • Swift

    • Java

    • C#

    // Before
    let fts = FullTextExpression.index("ftsIndex")
    let query = QueryBuilder
        .select(SelectResult.expression(Meta.id))
        .from(DataSource.collection(collection))
        .where(fts.match("search term"))
    
    // After
    let fts = FullTextFunction.index("ftsIndex")
    let query = QueryBuilder
        .select(SelectResult.expression(Meta.id))
        .from(DataSource.collection(collection))
        .where(FullTextFunction.match(fts, query: "search term"))
    // Before
    FullTextExpression fts = FullTextExpression.index("ftsIndex");
    Query query = QueryBuilder
        .select(SelectResult.expression(Meta.id))
        .from(DataSource.collection(collection))
        .where(fts.match("search term"));
    
    // After
    IndexExpression fts = Expression.fullTextIndex("ftsIndex");
    
    Query query = QueryBuilder
        .select(SelectResult.expression(Meta.id))
        .from(DataSource.collection(collection))
        .where(FullTextFunction.match(fts, "search term"));
    // Before
    var fts = FullTextExpression.Index("ftsIndex");
    var query = QueryBuilder
        .Select(SelectResult.Expression(Meta.ID))
        .From(DataSource.Collection(collection))
        .Where(fts.Match("search term"));
    
    // After
    var fts = FullTextFunction.Index("ftsIndex");
    var query = QueryBuilder
        .Select(SelectResult.Expression(Meta.ID))
        .From(DataSource.Collection(collection))
        .Where(FullTextFunction.Match(fts, "search term"));

    Vector Search Compatibility

    Couchbase Lite 4.0.0 is compatible only with Vector Search 2.0.0, due to dependency and build process changes.

    If you are using Vector Search in your application, you must upgrade to Vector Search 2.0.0 when migrating to Couchbase Lite 4.0.0.

    For more information about Vector Search 2.0.0, see the Vector Search release notes.

    Summary of Migration Actions

    The following table summarizes the key migration actions required when upgrading to Couchbase Lite 4.0.0:

    Area Action

    Document operations

    Use Collection APIs instead of Database for the default collection

    ReplicatorConfiguration

    Define collections up front using CollectionConfiguration for each collection. Remove dynamic add/remove logic.

    Couchbase Lite C

    Update struct field order if using positional initialization.

    .NET Configuration

    Adopt immutable, init-only configuration model.

    Replication pending documents

    Use collection-specific APIs.

    Endpoint Listener configuration

    Use constructors that accept collections

    TLSIdentity

    Use unified creation API with KeyUsages flag

    Logging

    Configure logs via immutable LogSink API

    QueryBuilder

    Replace removed DataSource, expression, and FTS functions

    Vector Search

    Upgrade to Vector Search 2.0.0