Published OnFebruary 10, 2026February 8, 2022

Using Ditto as a local database

Sometimes you might want to use Ditto just as a local database. Well you're in luck; straight out of the box it supports that!

Sometimes you might want to use Ditto just as a local database. Well you're in luck—straight out of the box it supports that!

Did you know that you can use Ditto in your apps as a regular document database? When we first created Ditto, we took three parallel tracks to building our entire system:

  1. The database portion: the primary way that customers use Ditto today
  2. Replication and CRDTs: how customers can mutate data in a distributed way and allow for automatic conflict resolution
  3. The mesh network: how devices can talk to each other in a heterogeneous way

Parts 2 and 3 only work if you call ditto.startSync(). However, we designed a system where the database portion is always capable of reading and writing information. That means if your application never sets up any network configuration permissions or calls ditto.startSync(), the instance will only act as a local database.

Basic Usage

The usage is incredibly simple. You interact with data using Ditto Query Language (DQL), a powerful SQL-like language designed specifically for Ditto. Here's an example using Swift:

let ditto = Ditto(identity: .offlinePlayground(appID: "my-app"))

// Inserting a document
try await ditto.store.execute(
    query: """
        INSERT INTO people
        DOCUMENTS (:person)
        ON ID CONFLICT DO UPDATE
    """,
    arguments: ["person": [
        "name": "Alice",
        "score": 21
    ]]
)

// Finding documents
let result = try await ditto.store.execute(
    query: """
        SELECT *
        FROM people
        WHERE name = :name
    """,
    arguments: ["name": "Alice"]
)

let item = result.items[0]
let score = item.value["score"]

// do something with the score
....
// Release item's value from memory
if (item.isMaterialized) {
  item.dematerialize()
}

and on Android with Kotlin:

val ditto = Ditto(androidDependencies, DittoIdentity.OfflinePlayground(appId = "my-app"))

// Inserting a document
ditto.store.execute(
    """
    INSERT INTO people
    DOCUMENTS (:person)
    ON ID CONFLICT DO UPDATE
    """,
    mapOf("person" to mapOf(
        "name" to "Alice",
        "score" to 21
    ))
)

// Finding documents
ditto.store.execute(
    """
    SELECT *
    FROM people
    WHERE name = :name
    """,
    mapOf("name" to "Alice")
).use { result ->
  val item = result.items.first()
  val score = item.value["score"]
  
  // do something with the score
  ....
  
// Release item's value from memory
  if (item.isMaterialized) {
      item.dematerialize()
  }
}

Note:  It’s recommended reading the documentation on managing Query Results to fully understand how to properly materialize and dematerialize results.

For more information on DQL and other supported programming languages, check out the DQL documentation. All data store operations work exactly the same as a syncing instance of Ditto.

Live Queries with Store Observers

Your application can react to data changes in real-time using store observers. Here is an example in Swift:

let ditto = Ditto(identity: .offlinePlayground(appID: "my-app"))

let observer = ditto.store.registerObserver(
    query: "SELECT * FROM cars"
) { queryResult in
    // Extract data into plain Swift objects immediately
    let cars: [Car] = queryResult.items.compactMap { item in
        let value = item.value
        item.dematerialize() // Release memory after extracting
        return Car(from: value)
    }
    updateUI(cars)
}

Note: ​Critical Memory Management​: QueryResults and QueryResultItems should be treated like database cursors. Always extract the data you need immediately and then close/dematerialize them. Never store QueryResultItems directly between observer emissions as this will cause memory bloat and potential crashes.

Controlling What Syncs with Sync Scopes

What if you want some data to sync and other data to remain local-only? Rather than managing multiple Ditto instances, you can use Sync Scopes to control sync behavior on a per-collection basis.

Sync Scopes allow you to specify how data in each collection is shared:

  • LocalPeerOnly: Data stays on the device only—no sync at all
  • SmallPeersOnly: Syncs only with other devices in the mesh (not Ditto Server)
  • BigPeerOnly: Syncs only with Ditto Server (not peer-to-peer)

Here's how to configure a collection as local-only using DQL:

// Set the "local_settings" collection to never sync
let syncScopes = [
    "local_settings": "LocalPeerOnly"
]

try await ditto.store.execute(
    query: "ALTER SYSTEM SET USER_COLLECTION_SYNC_SCOPES = :syncScopes",
    arguments: ["syncScopes": syncScopes]
)

// Now start sync for other collections
try ditto.startSync()

This approach gives you fine-grained control: some collections can sync across the mesh, others can sync only to the cloud, and others can remain completely local—all within a single Ditto instance.

Indexing for Performance

Ditto fully supports indexing to optimize query performance. You can create, drop, and view indexes directly through DQL:

-- Create an index on the "make" field in the cars collection
CREATE INDEX idx_cars_make ON cars (make)

-- You can add multiple indexes to the same collection (SDK 4.13 or higher)
CREATE INDEX idx_cars_model ON cars (model)

-- Drop an index
DROP INDEX idx_cars_make

-- View all indexes
SELECT * FROM system::dql_indexes

NOTE: From version 4.13.0, DQL supports union and intersect scans for queries with OR, IN, and AND operators. This allows the query optimizer to use multiple indexes simultaneously in a single query. For example, a query like WHERE status = ‘active’ OR priority = ‘high’ can leverage separate indexes on both status and priority fields, combining results through a union scan.
Creating indexes on frequently queried fields can significantly improve performance, especially for large datasets.

JavaScript Support

Our JavaScript SDK supports Ditto as a local database across multiple environments:

  • Node.js and Electron: Full persistence support with native file system access
  • Web Browsers: The npm package @dittolive/ditto uses WebAssembly with IndexedDB-backed persistence for modern browsers

Summary

Ditto is an end-to-end platform that can sync data regardless of connectivity from the edge to the cloud. But at its core, Ditto provides a powerful local document database with:

  • Full DQL support: SQL-like queries for reading and writing data
  • Store observers: Real-time reactions to data changes
  • Sync Scopes: Fine-grained control over what data syncs and what stays local
  • Indexing: Create indexes to optimize query performance
  • CRDT foundation: Even when used locally, your data is ready to sync if you ever need it

Whether you're building an offline-first app that needs occasional sync, or simply want a fast local database with a familiar query language, Ditto has you covered.

Ready to get started? Check out our quickstart guide or dive into the documentation.

This blog has been updated as of 2/10/26

Read more
Stories
February 17, 2026
The State of React Native in 2026
by
Aaron LaBeau
New Architecture, React Compiler, and What Developers Need to Know
Stories
February 10, 2026
The future is bright for Flutter in 2026
by
Aaron LaBeau
Flutter in 2026 is looking to be a real maturation of the platform, addressing fundamental architectural challenges while pushing the boundaries of what's possible in cross-platform development.