Zonai has built-in live queries. You do not need to poll, and you do not need a separate WebSocket or "realtime" product name. The framework word is stream.
Every table gets three streaming endpoints next to ordinary get / list / count. Prefer the generated Dart client (zonai_client) —
client.db.listen — over hand-rolled HTTP.
Endpoints#
| Operation | Method | Path | Body location | Rules / rate-limit bucket |
|---|---|---|---|---|
stream-one |
GET |
/db/stream |
?body=<JSON> |
canView / getPolicy |
stream-list |
GET |
/db/stream/list |
?body=<JSON> |
canList / limitPolicy |
stream-count |
GET |
/db/stream/count |
?body=<JSON> |
canCount / countPolicy |
Payload types live in zonai_schema: StreamBody, StreamListBody,
StreamCountBody.
Same conventions as other DB routes: table name in the JSON body, never in the path. Streams use the same authorization as their non-streaming counterparts.
What "live" means#
The server keeps the HTTP response open and pushes a new JSON payload whenever the underlying SQLite query result changes (inserts, updates, deletes that affect the query). Cancel the client subscription (or close the connection) when you are done.
This is not Server-Sent Events branding and not a separate pub/sub channel — it is a first-class /db/stream*
route. Searching docs for realtime, socket, SSE, or EventSource
will miss it; search for stream or listen.
Prefer zonai_client#
import 'package:zonai_client/zonai_client.dart';
import 'package:zonai_schema/zonai_schema.dart';
final client = ZonaiClient.instance;
// One row — where is required (usually id equality).
final sub = client.db.listen
.one(
body: StreamBody(
table: 'tasks',
where: Eq('id', 'tk_abc123'),
),
fromJson: (row) => row,
)
.listen((row) {
// Fired on connect and whenever that row changes.
});
// Matching list
client.db.listen
.list(
body: StreamListBody(
table: 'tasks',
where: Eq('isComplete', false),
limit: 50,
),
fromJson: (row) => row,
)
.listen((rows) { /* ... */ });
// Count
client.db.listen
.count(body: StreamCountBody(table: 'tasks'))
.listen((total) { /* ... */ });
await sub.cancel();
See Dart Client — Database for the full client API.
Raw HTTP#
Same ?body= pattern as GET /db / /db/list / /db/count:
GET /db/stream?body={"table":"tasks","where":{"id":{"eq":"tk_abc123"}},"expand":[]}
GET /db/stream/list?body={"table":"tasks","where":{"isComplete":{"eq":false}},"limit":20}
GET /db/stream/count?body={"table":"tasks"}
Include Authorization: Bearer <jwt> when rules require it. Keep the connection open and read successive JSON events until you cancel.
Rules and rate limits#
There are no separate canStream* rule methods. Streaming reuses:
canView→/db/streamcanList→/db/stream/listcanCount→/db/stream/count
Rate limits reuse getPolicy, limitPolicy, and countPolicy respectively.
When you still might poll#
Polling is only a fallback if you cannot hold a long-lived HTTP connection (some constrained proxies). For Flutter / Dart apps talking to Zonai, use
client.db.listen first.
