The next evolution of SQLite is here! Read Announcement

Introducing Databases Anywhere with Turso Sync

Nikita SivukhinNikita Sivukhin
Cover image for Introducing Databases Anywhere with Turso Sync

Modern applications need to work everywhere. In browsers, on servers, locally, and offline. Users expect seamless experiences whether they’re connected or not.

Today, we’re launching Turso Sync, enabling local SQLite databases to sync anywhere effortlessly, including to and from Turso Cloud.

#Usage

The @tursodatabase/sync package provides the same API as the main @tursodatabase/database package, with only one difference that the connect method additionally requires a remote database URL and an authentication token for the database. Also, sync package provides push and pull methods to interact with the remote and checkpoint method for optimizing local WAL size.

For browser use, you'll need the dedicated @tursodatabase/sync-wasm package. See the blog post for more details about the browser implementation of the main database package.

And that's it! If you have application using @tursodatabase/database package — you only need to update connection method and write pull / push loops to sync with remote:

import { connect } from "@tursodatabase/sync"
const db = await connect({
  path: "local.db",
  // url taken from Dashboard or from CLI: turso db show <db> --url
  url: "libsql://<db>-<org>.<aws-region>.turso.io",
  // token generated in the Dashboard or through CLI: turso db tokens create <db>
  authToken: "...",
  // long-polling timeout for pull operation
  longPollTimeoutMs: 5000,
});
async function checkpoint() {
  try { 
    if ((await db.stats()).mainWal > 100000) { await db.checkpoint(); } 
    setTimeout(checkpoint, 5000); 
  }
  catch (e) { console.info('checkpoint', e); setTimeout(checkpoint, 5000); }
}
async function pull() {
  try { 
    // update UI if something new was pulled form the remote
    if (await db.pull()) { await updateUI() }; 
    setTimeout(pull, 0); 
  }
  catch (e) { console.info('pull', e); setTimeout(pull, 5000); }
}
async function push() {
  try { 
    if ((await db.stats()).operations > 0) { await db.push(); } 
    setTimeout(push, 100); 
  }
  catch (e) { console.info('push', e); setTimeout(push, 5000); }
}
// run pull/push/checkpoint operations in the background
pull(); push(); checkpoint();

#Authentication

Offline-first applications with local sync support require more advanced authentication capabilities. That's why, specifically for the sync launch, we've extended Turso Cloud authentication with the following features:

  1. Fine-grained token permissions — limit access per table and per operation:

    • Supported data operations: data_read, data_add, data_update, data_delete

    • Supported schema operations: schema_add, schema_update, schema_delete

    • Attach fine-grained permissions to your token using the Turso CLI:

      turso db tokens create <db> -p all:data_read -p table:data_update
      
  2. External Auth provider support

    • Add an external JWKS URL containing public keys from your authentication provider, and use provider-issued tokens to authenticate with Turso.

    • JWKS is configured per organization and can be managed via the dashboard or CLI:

      turso org jwks ...
      
    • Generate a JWT template for your provider through the CLI:

      turso org jwks template --database <db> -p all:data_read,data_add
      

During the Turso Beta, only Clerk and Auth0 are supported as OIDC providers.

See the documentation for more details about authentication features.

#Conflict resolution

By default, sync uses row-level logical logging with a Last-Push-Wins conflict resolution strategy. This means that if the same row is modified on different devices, the version that is pushed last will take precedence — regardless of the local commit time.

This is a good default conflict resolution strategy, but if an application requires more sophisticated logic, the sync package exposes a custom transform hook that is applied to all mutations before they are sent to the remote. Using this hook, developers can implement more advanced conflict resolution and merge strategies tailored to their application's needs.

const db = await connect({ 
  path: '...', url: '...', authToken: '...', 
  transform: mutation => ({
    operation: 'rewrite',
    stmt: {
      sql: `UPDATE counter SET value = value + ? WHERE key = ?`,
      values: [m.after.value - m.before.value, m.after.key]
    }
  })
});

#Technical details

Under the hood, the sync package maintains a regular local tursodatabase instance with all standard components in place — the database file, WAL, and a few additional files used to preserve metadata.

Synchronization works in a hybrid fashion:

  1. Local changes are pushed to the remote as logical mutations, allowing for flexible conflict resolution.
  2. Remote changes are pulled as physical pages, ensuring the local replica eventually becomes byte-for-byte identical to the remote database state.

The remote database acts as the source of truth, which motivates the use of physical updates for the pull operation.

This also means that fully distributed, peer-to-peer synchronization between devices is not supported in the current implementation.

The database's WAL is retained to extract local changes and "rebase" them after a successful pull operation. Since this can become problematic if pulls are infrequent, the package provides a sync-aware checkpoint method — db.checkpoint() — which optimizes the WAL while preserving all necessary information in separate files on disk.

#What's Coming Next?

We're excited about the Beta launch of the sync feature. It opens up opportunities for significant optimizations and architectural shifts across multiple domains:

  • Front-end applications can now implement offline-first behavior effortlessly.
  • Machine learning pipelines can use @tursodatabase/sync to perform training operations locally and periodically sync with the remote database for backup or distributed coordination.
  • Backend applications can achieve microsecond-level read and write latencies by leveraging the sync package for efficient data access.

To make this feature truly awesome, we already have several items on our roadmap:

  • Partial sync, allowing users to fetch only the data they need — lazily or on demand.
  • CDC optimizations to reduce disk usage and make change sets more compact before sending them to the remote.
  • Broader language support — with Go, Python, and JavaScript as our current priorities.
  • Mobile ecosystem integration, bringing sync capabilities to native mobile applications.