Realtime Subscriptions
For most Flutter lists, use liveTable() or live(). They emit the current row set. Use liveEvents() only when you need raw insert/update/delete protocol events.
Live SQL must be SELECT ... FROM ... WHERE ... — no ORDER BY or LIMIT.
Need a local cache that works offline? Use Kalam Sync instead of reconciling events yourself.
Start with the Flutter inbox example for a complete widget with loading, empty, error, retry, and cleanup behavior.
Which API
liveTable(name)—SELECT * FROM name, materialized rowslive(sql)— custom SELECT, materialized rowsliveEvents(sql)— Ack / snapshot batches / insert / update / delete
Materialized live rows
live<T>(...) keeps the current row set updated inside the SDK
and emits the latest list whenever the query result changes.
Use liveTable<T>(...) when SELECT * FROM table is enough:
If your live query uses a natural or composite key instead of a plain id
column, pass keyColumns so reconciliation still stays inside the shared Rust
core:
You can also cap the in-memory materialized row set with limit, map rows
directly into app models with mapRow, and observe checkpoint progress with onCheckpoint. For durable cached state,
use Kalam Sync.
Materialized rows vs change events
liveEvents(...)gives you protocol-level events and incremental changes.live(...)andliveTable(...)give you the fully reconciled current row list.- Materialized helpers are usually the better default for UI code.
- Low-level events are better when you need custom reconciliation, audit logging, or protocol inspection.
Materialized-row limits
The SQL for materialized live rows still follows the same strict supported shape:
SELECT ... FROM ... WHERE ...- no
ORDER BY - no
LIMIT
The controls apply at different stages:
batchSizechunks the initial snapshot sent by the server.lastRowsselects how much history to rewind before live changes begin.limitcaps the materialized live row set the SDK keeps after startup.
Keep Dart-side presentation logic for sorting and grouping after rows arrive.
Use limit only when the materialized live state itself should stay bounded.
Raw events (liveEvents)
Snapshot vs live changes
Most subscriptions follow this pattern:
AckEventconfirms the subscription and includes schema metadata.- One or more
InitialDataBatchevents deliver the initial snapshot. InsertEvent/UpdateEvent/DeleteEventdeliver live changes.
If you only care about live changes, you can ignore InitialDataBatch after initial UI hydration.
Controlling the initial snapshot
batchSize
If the initial query returns many rows, the server can send it in batches. batchSize controls the maximum rows per snapshot batch.
You will receive one or more InitialDataBatch events with hasMore=true, followed by hasMore=false and then live changes.
lastRows
lastRows asks the server to “rewind” and include the last N rows before live changes begin. This is useful for chat timelines or activity feeds.
subscriptionId
Provide an explicit subscriptionId if you want stable IDs for client-side bookkeeping:
from
from accepts a VersionId checkpoint. Use it only when restoring the matching
cached data for the same query and authenticated identity. A cursor alone does
not reconstruct a complete list after an app restart; ordinary screens should
start with a fresh snapshot and omit from.
For materialized rows, onCheckpoint exposes checkpoint.lastVersionId. This
callback is not a transaction that atomically commits your app’s data and cursor.
Use Kalam Sync for durable local state.
Replay is bounded. A delivery gap, expired cursor, or changed history can require resubscription or a fresh snapshot. Do not present retained rows as current until recovery completes. See the WebSocket recovery contract.
Listing active subscriptions
Use getSubscriptions() to inspect all subscriptions on the shared connection.
For server-side observability while debugging, query SELECT * FROM system.live.
That view is node-local and reflects the subscriptions currently held in memory
on the node you query. It is not persisted to RocksDB and it is not replicated
through Raft.
Cancelling a subscription
The SDK returns a Stream<ChangeEvent>. Cancel by cancelling your StreamSubscription.
Cancellation stays lightweight on the Dart side. The SDK cancels the Rust subscription handle on the shared connection, waits briefly for the pull loop to unblock, and then closes the stream. That avoids leaving a hanging Dart-side pull loop after a widget or provider disposes.
Data types
All subscription events expose rows as Map<String, KalamCellValue> for type-safe access:
InitialDataBatch.rows→List<Map<String, KalamCellValue>>InsertEvent.row/InsertEvent.rowsUpdateEvent.columnIndexes/UpdateEvent.row/UpdateEvent.oldRow/UpdateEvent.oldRowsDeleteEvent.row/DeleteEvent.oldRows
UPDATE event behavior
For UpdateEvent, row and rows are sparse typed maps reconstructed from the Ack schema.
They contain only the changed columns plus the primary key columns and _seq.
oldRow contains the previous values for that same sparse column set.
columnIndexes exposes the original positional update layout from the wire protocol. It lines up
with the values sent on the Rust bridge raw event if you need exact sparse-column ordering.
Use the typed accessors to extract values:
These event accessors are already decoded from JSON row payloads; no manual List<List<dynamic>> parsing is needed.
See Cell Values for the full KalamCellValue API.