LogoZonai

Streaming (Live Queries)

Long-lived HTTP streams that push row, list, and count updates when SQLite data changes — no polling required.

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/stream
  • canList/db/stream/list
  • canCount/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.