Skip to Content
Kalam Sync

Kalam Sync

kalam_sync keeps a local Drift cache so Flutter UI works offline, then syncs live SQL through kalam_link.

Need a server first? kalam init or the server quick start.

Fastest path

BASH
kalam init --yes --languages dart --template simple-livekalam schema gen --languages dartkalam dev

That writes a Flutter starter, schema.sql, and KalamTables.*. Or add the package yourself:

pubspec.yamlYAML
dependencies:kalam_sync: ^0.6.0-rc.0
BASH
flutter pub add kalam_sync

Open one account-scoped cache

DART
import 'package:flutter/material.dart';import 'package:kalam_sync/kalam_sync.dart'; Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();   final kalam = await Kalam.open(    url: 'http://localhost:2900',    subject: 'dev-user',    namespace: 'app',    authProvider: () async => Auth.basic('root', 'kalamdb123'),  );   runApp(KalamScope(kalam: kalam, child: const App()));}

subject is required. The server URL, namespace, and authenticated subject form the local database identity, so one user’s cached rows and queued actions cannot be opened or flushed as another user.

Production apps should pass the signed-in user id and return Auth.jwt(...) from authProvider.

Kalam.open() initializes the Rust runtime and opens SQLite immediately. The WebSocket stays lazy until the first consumer or queued action needs it. KalamScope pauses and resumes sync with the Flutter app lifecycle.

Bind a table and subscribe

kalam schema gen --languages dart writes row classes and KalamTables.* specs from schema.sql. Bind that spec — do not invent a second model:

DART
final todos = kalam.table(KalamTables.todos); late final KalamSyncSubscription todoSync; Future<void> startTodos() async {  todoSync = await kalam.subscribe(    todos.consumer(sql: 'SELECT * FROM app.todos', batchSize: 250),  );} Future<void> addTodo(String title) {  return todos.insert(    Todos(      id: Kalam.id(),      title: title,      completed: false,    ),    actionId: Kalam.id(),  );}

insert, update, and delete change the local row and enqueue one generic Kalam DML action in the same SQLite transaction. todos.watch() is always local and keeps working offline.

Live SQL must stay SELECT ... FROM ... WHERE .... Do not put ORDER BY or LIMIT in the consumer SQL.

Watch rows in the UI

DART
StreamBuilder<List<KalamSyncedRow<Todos>>>(  stream: todos.watchWithSyncState(),  builder: (context, snapshot) {    final rows = snapshot.data ?? const <KalamSyncedRow<Todos>>[];    return ListView(      children: [        for (final row in rows)          ListTile(            title: Text(row.value.title),            trailing: Icon(              row.isSynced ? Icons.cloud_done : Icons.cloud_upload,            ),          ),      ],    );  },)

watch() returns decoded rows only. Use watchWithSyncState() when the UI needs pending, retry, failure, or awaiting-server-echo state beside each row.

That’s enough for a first Flutter list. The rest of this page is optional.

Subscribe inside a widget

Start only the query the screen needs, and cancel it when the widget unloads:

DART
import 'dart:async'; class TodoListState extends State<TodoList> {  late final KalamTableBinding<Todos> todos;  KalamSyncSubscription? sync;   @override  void initState() {    super.initState();    final kalam = KalamScope.read(context);    todos = kalam.table(KalamTables.todos);    kalam        .subscribe(todos.consumer(sql: 'SELECT * FROM app.todos'))        .then((value) {          if (mounted) {            sync = value;          } else {            unawaited(value.cancel());          }        });  }   @override  void dispose() {    sync?.cancel();    super.dispose();  }}

Use KalamScope.read(context) in initState. KalamScope.of(context) rebuilds when the session changes.

consumer() accepts the same params list as query():

DART
messages.consumer(  sql: r'SELECT * FROM app.messages WHERE conversation_id = $1',  params: [conversationId],)

Later: catch-up, table modes, durability

Catch up in a headless isolate

The same live query can drain a bounded backlog, then disconnect:

DART
final result = await Kalam.catchUp(  url: serverUrl,  subject: userId,  namespace: 'app',  authProvider: () async => Auth.jwt(await tokens.freshAccessToken()),  consumers: [todos.consumer(sql: 'SELECT * FROM app.todos')],  rowLimit: 100,  timeout: const Duration(seconds: 30),);

Catch-up resumes from: the SQLite-committed checkpoint and uses batchSize as the row limit.

Table modes

ModeLocal writesTypical use
KalamSyncMode.bidirectionalinsert / update / delete queue generic DMLTodos, settings, conversations
KalamSyncMode.replicaOnlyDirect DML throws; enqueue a custom actionMessages, workflows the backend owns

Stream tables generate as replicaOnly. User and shared tables generate as bidirectional. Override with spec.copyWith(mode: ...) when the backend must stay authoritative.

Custom actions, generated queues, and named steps are in Offline Actions.

What stays durable

  • Local mirror row + outbox enqueue are one Drift transaction.
  • Applied server row + sequence checkpoint are one Drift transaction.
  • Server deliveries are acknowledged only after that transaction commits.
  • Duplicate or older events are ignored.
  • Actions, retry metadata, optimistic rows, and checkpoints survive process restart.
  • pause() / resume() cancel and reopen subscriptions from the SQLite-committed checkpoint.

kalam_sync uses kalam_link.liveEventsWithAck. Existing liveEvents() callers keep automatic progress; sync subscriptions resume from the committed cursor after reconnect or process restart.

Next

Last updated on