electricsql 0.8.0 copy "electricsql: ^0.8.0" to clipboard
electricsql: ^0.8.0 copied to clipboard

A Dart implementation for ElectricSQL (electric-sql.com) client.

Tests E2E

pub package pub package pub package

Electric Dart

🛠️ WORK IN PROGRESS 🛠️

Electric is currently in public alpha phase, and the Dart client is currently being developed introducing the new features from the official client as they come out. For development updates make sure to check out the official ElectricSQL Discord server, as well as the official Javascript client


Unofficial Dart client implementation for Electric.

Client based on the Typescript client from the clients/typescript subfolder from electric git repository

Reference implementation: #

  • NPM package.
  • Version v0.12.0
  • Commit: f322afcec33c29610f72baae922abc86fd604caa

What is ElectricSQL? #

ElectricSQL is a local-first sync layer for modern apps. Use it to build reactive, realtime, local-first apps using standard open source Postgres and SQLite.

The ElectricSQL client provides a type-safe database client autogenerated from your database schema and APIs for Shape-based sync and live queries. The client combines with the drift package to provide a seamless experience for building local-first apps in Dart.

Local-first is a new development paradigm where your app code talks directly to an embedded local database and data syncs in the background via active-active database replication. Because the app code talks directly to a local database, apps feel instant. Because data syncs in the background via active-active replication it naturally supports multi-user collaboration and conflict-free offline.

Introduction & Live Demos

Run the Todos example #

This is a simple Todos app which can sync across all the platforms supported by Flutter (iOS, Android, Web, Windows, macOS and Linux).

Instructions

Electric Flutter

Quickstart #

Quickstart to integrate Electric into your own Flutter app.

Usage #

Instantiate #

To handle type conversions and reactivity of the sync system, this package can be integrated with drift. To start using Electric, you need to electrify your database as follows.

import 'package:electricsql/electricsql.dart';
import 'package:electricsql_flutter/drivers/drift.dart';

// This would be the Drift database
AppDatabase db;

final electric = await electrify<AppDatabase>(
    dbName: '<db_name>',
    db: db,
    // Bundled migrations. This variable is autogenerated using
    // `dart run electricsql_cli generate`
    migrations: kElectricMigrations,
    config: ElectricConfig(
        // Electric service URL
        url: 'http://<ip>:5133',
        // logger: LoggerConfig(
        //     level: Level.debug, // in production you can use Level.off
        // ),
    ),
);

// https://electric-sql.com/docs/usage/auth
// You can use the functions `insecureAuthToken` or `secureAuthToken` to generate one
final String jwtAuthToken = '<your JWT>';

// Connect to the Electric service
await electric.connect(jwtAuthToken);

Sync data #

Shapes are the core primitive for controlling sync in the ElectricSQL system. Shapes docs

Wait for sync finished

If the shape subscription is invalid, the first promise will be rejected. If the data load fails for some reason, the second promise will be rejected.

// Resolves once the shape subscription is confirmed by the server.
final shape = await electric.syncTable(<some_shape>);

// Resolves once the initial data load for the shape is complete.
await shape.synced

Sync a full table

final shape = await electric.syncTable(db.projects);

Sync a filtered set of rows

final shape = await electric.syncTable(
    db.projects,
    // Boolean expression with drift syntax
    where: (p) => p.status.equals('active') & p.title.contains('foo'),
);

Sync deep nested shapes

The $relations field is autogenerated by the Electric CLI as part of your drift schema. In this example, projects are synced with all its related content (project issues, issue comments and comment authors).

final shape = await electric.syncTable(
    db.projects,
    include: (p) => [
        SyncInputRelation.from(
            p.$relations.issues,
            include: (i) => [
                SyncInputRelation.from(
                    i.$relations.comments,
                    include: (c) => [
                        SyncInputRelation.from(c.$relations.author),
                    ],
                ),
            ],
        ),
    ],
);

Read data #

Bind live data to the widgets. This can be possible when using drift + its Stream queries.

Create a Stream query with the drift watch API

AppDatabase db;
// Since we've electrified it, we can now read from the drift db as usual.
// https://drift.simonbinder.eu/docs/dart-api/select/

// Watch query using drift Dart API
final Stream<List<Todo>> todosStream = db.select(db.todos).watch();

// Watch query using raw SQL
final Stream<List<QueryRow>> rawTodosStream = db.customSelect(
    'SELECT * FROM todos',
    // This is important so that Drift knows when to run this query again
    // if the table changes
    readsFrom: {db.todos},
).watch();

Make widgets reactive

// Stateful Widget + initState
todosStream.listen((List<Todo> liveTodos) {
    setState(() => todos = liveTodos.toList());
});

// StreamBuilder
StreamBuilder<List<Todo>>(
    stream: todosStream,
    builder: (context, snapshot) {
        // Manage loading/error/loaded states
        ...
    },
);

Write data #

You can use the original database instance normally so you don't need to change your database code at all. The data will be synced automatically, even raw SQL statements.

AppDatabase db;

// Using the standard Drift API
// https://drift.simonbinder.eu/docs/dart-api/writes/
await db.into(db.todos).insert(
    TodosCompanion.insert(
        title: 'My todo',
        createdAt: DateTime.now(),
    ),
);

// Or raw SQL
// WARNING: Even though this is possible, it's not recommended to use raw SQL to
// insert/update data as you would be bypassing certain formats that Electric
// expects for some special data types like UUIDs, timestamps, int4, etc...
//
// It's perfectly safe to use raw SQL for SELECT queries though, you would only
// need to tell drift what tables are being used in the query so that Stream queries
// work correctly
//
// If you really need a raw INSERT/UPDATE you can encode the parameters using the
// `TypeConverters` class.
// Like: `TypeConverters.timestampTZ.encode(DateTime.now())`
await db.customInsert(
    'INSERT INTO todos (title, created_at) VALUES (?, ?)',
    variables: [
        Variable('My todo'),
        Variable(TypeConverters.timestampTZ.encode(DateTime.now())),
    ],
    updates: {db.todos}, // This will notify stream queries to rebuild the widget
);

This automatic reactivity works no matter where the write is made — locally, on another device, by another user, or directly into Postgres.

More about ElectricSQL #

Check out the official docs from ElectricSQL here to look at live demos, API docs and integrations.

DevTools #

The package provides a DevTools extension to interact with the Electric service during development. That is: check the status of the service connectivity, inspect the table schemas, delete the local database, check the status of the shape subscriptions...

Reset local database #

To add support for the reset local database button you need to tell Electric how to reset the local database. On non-web platforms is simply closing the database connection and deleting the file. You can see a cross platform implementation in the todos_flutter example.

ElectricDevtoolsBinding.registerDbResetCallback(
    electricClient, // output of `electrify`
    () async {
        await db.close();
        await deleteDbFile(db); 
    },
);

Development instructions for maintainers and contributors #

Dart 3.x and Melos required

dart pub global activate melos

Bootstrap the workspace #

melos bs

Generate the Protobuf code #

Install the protoc_plugin Dart package.

dart pub global activate protoc_plugin

To generate the code

melos generate_proto

Run the tests #

melos test:all