Drivers

ElectricSQL works with existing SQLite database driver libraries. The principle is that you import and use your existing driver as normal, after “electrifying” it by passing the original driver instance to an electrify function provied by the ElectricSQL client library.

This guide has instructions for integrating with drivers for the following platforms. Note that if we don’t currently support your platform, you can implement your own generic driver integration.

Mobile

We currently support JavaScript based mobile applications. Let us know on Discord if you’re interested in using ElectricSQL with other platforms.

Cordova

We support Apache Cordova via the cordova-sqlite-storage driver. Make sure you’ve bundled your migrations, instatiate your driver as normal and then electrify it:

import { electrify } from 'electric-sql/cordova'
import { data } from './migrations'

const config = {
  app: '<YOUR APP SLUG>',
  migrations: data.migrations
}

const opts = {
  name: 'example.db',
  location: 'default'
}

document.addEventListener('deviceready', () => {
  window.sqlitePlugin.openDatabase(opts, async (original) => {
    const db = await electrify(original, config)

    // Use as normal, e.g.:
    db.executeSql('select foo from bar', [], (results) => {
      console.log('query results: ', results)
    })
  })
})

Expo

We support Expo via expo-sqlite. Make sure you’ve bundled your migrations, instatiate your driver as normal and then electrify it:

import * as SQLite from 'expo-sqlite'

import { electrify } from 'electric-sql/expo'
import { data } from './migrations'

const config = {
  app: '<YOUR APP SLUG>',
  migrations: data.migrations
}

SQLite.openDatabase('example.db', null, null, null, async (original) => {
  const db = await electrify(original, config)

  // Use as normal, e.g.:
  db.transaction((tx) => {
    tx.executeSql('select foo from bar', [], (_tx, results) => {
      console.log('query results: ', results)
    })
  })
})

See the Expo example application for more information.

React Native

We support React Native via the react-native-sqlite-storage driver. Make sure you’ve bundled your migrations, instatiate your driver as normal and then electrify it:

import SQLite from 'react-native-sqlite-storage'
import { electrify } from 'electric-sql/react-native'
import { data } from './migrations'

// You can use the driver with or without the promise API enabled.
// Either way, you need to pass in a flag to the `electrify` function
// below to indicate whether promises are enabled or not.
const promisesEnabled = true
SQLite.enablePromise(promisesEnabled)

const config = {
  app: '<YOUR APP SLUG>',
  migrations: data.migrations
}

SQLite.openDatabase('example.db')
  .then(original => electrify(original, promisesEnabled, config))
  .then(db => { // Use as normal, e.g.:
    db.transaction((tx) => {
      tx.executeSql('SELECT 1', [], (_tx, results) => {
        console.log('query results: ', results)
      })
    })
  })

See the React Native example application for more information.

Web

ElectricSQL supports running in the browser using SQL.js, which is a port of SQLite to WASM. We’re also tracking the Google Chrome project to standardise SQLite in the browser:

Our intention is to empower developers to create their own solutions for structured storage and we’re therefore working with the SQLite team to create a SQLite implementation over WebAssembly. This solution will replace Web SQL.

SQL.js

ElectricSQL runs SQL.js with James Long’s absurd-sql system for persistence. This runs in a web worker (which we also use to keep background replication off the main thread). As a result, the electrified db client provides an asynchronous version of a subset of the SQL.js driver interface.

First create a worker.js file that imports and starts an ElectricWorker process:

// worker.js
import { ElectricWorker } from 'electric-sql/browser'

ElectricWorker.start(self)

Then, in your main application (noting that you pass your config into the SQL.openDatabase call):

import { initElectricSqlJs } from 'electric-sql/browser'
import { data } from './migrations'

const config = {
  app: '<YOUR APP SLUG>',
  migrations: data.migrations
}

// Start the background worker.
const url = new URL('./worker.js', import.meta.url)
const worker = new Worker(url, {type: "module"})

// Electrify the SQL.js / absurd-sql machinery and then open
// a persistent, named database.
initElectricSqlJs(worker, {locateFile: file => `/${file}`})
  .then(SQL => SQL.openDatabase('example.db', config))
  .then(db => db.exec('SELECT 1'))
  .then(results => console.log('results: ', results))

This gives you persistent, local-first SQL with active-active replication in your web browser 🤯. Use the db client as normal (see the API documentation here), with the proviso that the methods are now async (they return promises rather than direct values).

 Note that the path to the worker.js must be relative and the worker.js file must survive any build / bundling process. This is handled automatically by most bundlers, including Esbuild (as of #2508), Rollup and Webpack.

See the Web example application for more information.

Edge

We support edge applications using Node.js. We aim to support other languages soon (starting with Elixir). In the meantime, you can use ElectricSQL from any server-side application via Postgres.

Node

We support Node.js via the better-sqlite3 driver. Note that this is not the default node-sqlite3 adapter. See this rationale for context.

Make sure you’ve bundled your migrations, instatiate your driver as normal and then electrify it:

import Database from 'better-sqlite3'

import { electrify } from 'electric-sql/node'
import { data } from './migrations'

const config = {
  app: '<YOUR APP SLUG>',
  migrations: data.migrations
}

const opts = {
  name: 'example.db'
}

const original = new Database('example.db')
electrify(original, config).then((db) => {
  // Use as normal, e.g.:
  const stmt = db.prepare('select foo from bar')
  const results = stmt.all()

  console.log('query results: ', results)
})

See the Node example application for more information.

Postgres

As well as extending SQLite in the local device, ElectricSQL also optionally provisions a geo-distributed cloud Postgres database. Backend applications can use this to integrate with an ElectricSQL system. Either for primary OLTP workloads, or for management and reporting purposes.

Postgres credentials are provided when you create an application using the ElectricSQL console. You can then connect as standard using any Postgres driver. It’s entirely a standard, fully-featured Postgres system. The differences to be aware of are:

  1. your credentials are scoped to a user without DDL access permissions
  2. migrations must be applied using electric migrations sync

Generic

You can integrate any SQLite database driver by adapting it to the ElectricSQL DatabaseAdapter interface.

Probably the best guidance for this is to look at the existing driver implementations and to note the interface signatures in drivers/generic/database.ts.

First ensure that your driver client has an isGenericDatabase property set to true:

MyDriverClass.prototype.isGenericDatabase = true

Then provide an ElectricDatabase class that implements ProxyWrapper:

import { Database } from 'electric-sql/generic'
import { ElectricNamespace } from 'electric-sql/electric'
import { ProxyWrapper } from 'electric-sql/proxy'

export class ElectricDatabase implements ProxyWrapper {
  _db: Database

  electric: ElectricNamespace

  constructor(db: Database, namespace: ElectricNamespace) {
    this._db = db
    this.electric = namespace
  }

  _setOriginal(db: Database): void {
    this._db = db
  }
  _getOriginal(): Database {
    return this._db
  }
}

Then implement DatabaseAdapter:

import { Database } from 'electric-sql/generic'
import {
  BindParams,
  QualifiedTablename,
  Row,
  parseTableNames
} from 'electric-sql/util'

export class DatabaseAdapter {
  db: Database

  constructor(db: Database) {
    this.db = db
  }

  async run(statements): Promise<void> {
    // XXX implement using your driver API.
    await this.db.runStatementsInTransaction(statements)
  }

  async query(sql: string, bindParams: BindParams = []): Promise<Row[]> {
    // XXX implement using your driver API.
    return await this.db.runQuery(sql, bindParams)
  }

  tableNames(sql: string): QualifiedTablename[] {
    return parseTableNames(sql)
  }
}

Wrap up into an bespoke electrify function:

import {
  ElectricNamespace,
  ElectrifyOptions,
  electrify as baseElectrify
} from 'electric-sql/electric'

import {
  BundleMigrator,
  Database,
  ElectrifiedDatabase,
  EventNotifier,
  globalRegistry
} from 'electric-sql/generic'

// Your implementations
import { DatabaseAdapter } from './adapter'
import { ElectricDatabase } from './database'

export const electrify = async (db: Database, dbName: string, opts: ElectrifyOptions = {}): Promise<ElectrifiedDatabase> => {
  const adapter = opts.adapter || new DatabaseAdapter(db)
  const migrator = opts.migrator || new BundleMigrator(adapter, opts.migrations)
  const notifier = opts.notifier || new EventNotifier(dbName)
  const registry = opts.registry || globalRegistry

  const namespace = new ElectricNamespace(adapter, notifier)
  const electric = new ElectricDatabase(db, namespace)

  const electrified = await baseElectrify(dbName, db, electric, adapter, migrator, notifier, registry)
  return electrified as unknown as ElectrifiedDatabase
}

You can now electrify your database driver:

import { data } from '../migrations'
import { electrify } from './index'

const config = {
  app: '<YOUR APP SLUG>',
  migrations: data.migrations
}

const original = new MyDriverClass('example.db')
electrify(original, 'example.db', config)
  .then(db => {
    // ... use electrified db client
  })

Next step