Every table registered with table() or authTable() automatically gets these operations. The table name is always part of the request body — it is never in the path.
Endpoints#
| Operation | Method | Path | Body location |
|---|---|---|---|
get |
GET |
/db |
?body=<JSON> |
list |
GET |
/db/list |
?body=<JSON> |
count |
GET |
/db/count |
?body=<JSON> |
stream-one |
GET |
/db/stream |
?body=<JSON> |
stream-list |
GET |
/db/stream/list |
?body=<JSON> |
stream-count |
GET |
/db/stream/count |
?body=<JSON> |
create | POST | /db | JSON body |
create many |
POST |
/db/many |
JSON body |
update | PATCH | /db | JSON body |
update many |
PATCH |
/db/many |
JSON body |
delete | DELETE | /db | JSON body |
delete many |
DELETE |
/db/many |
JSON body |
Live updates use the stream-* routes (and client.db.listen in zonai_client). See
Streaming (Live Queries) — do not poll unless your environment cannot hold a long-lived HTTP connection.
Request / Response Shape#
get — GET /db#
Pass the body as a URL-encoded JSON string in the body query parameter:
GET /db?body={"table":"tasks","where":{"id":{"eq":"tk_abc123"}}}
Response:
{ "data": { "id": "tk_abc123", "title": "Buy groceries", "isComplete": false, ... } }
list — GET /db/list#
GET /db/list?body={"table":"tasks","limit":20,"offset":0,"order_by":[{"column":"createdAt","direction":"desc"}]}
Response:
{ "data": { "items": [...], "total": 42 } }
total is the full count of matching rows, ignoring limit/offset.
count — GET /db/count#
GET /db/count?body={"table":"tasks","where":{"isComplete":{"eq":true}}}
Response:
{ "data": 7 }
stream-one — GET /db/stream#
Long-lived connection. Pushes the matching row whenever it changes. Requires where. Prefer
client.db.listen.one in Dart. See Streaming.
GET /db/stream?body={"table":"tasks","where":{"id":{"eq":"tk_abc123"}},"expand":[]}
stream-list — GET /db/stream/list#
Long-lived connection. Pushes the full matching page whenever the result set changes.
GET /db/stream/list?body={"table":"tasks","where":{"isComplete":{"eq":false}},"limit":20}
stream-count — GET /db/stream/count#
Long-lived connection. Pushes a new count whenever matching rows change.
GET /db/stream/count?body={"table":"tasks"}
Do not poll these endpoints on a timer. Keep the HTTP connection open. Searching for "realtime"/"SSE"/"WebSocket" will miss them — the framework word is stream.
create — POST /db#
// Request body
{ "table": "tasks", "object": { "title": "Buy groceries", "isComplete": false } }
// Response
{ "data": { "id": "tk_abc123", "title": "Buy groceries", "isComplete": false, "createdAt": "...", "updatedAt": "..." } }
create many — POST /db/many#
// Request body
{
"table": "tasks",
"objects": [
{ "title": "Buy groceries", "isComplete": false },
{ "title": "Walk the dog", "isComplete": false }
]
}
// Response
{ "data": [
{ "id": "tk_abc123", "title": "Buy groceries", "isComplete": false, "createdAt": "...", "updatedAt": "..." },
{ "id": "tk_def456", "title": "Walk the dog", "isComplete": false, "createdAt": "...", "updatedAt": "..." }
] }
update — PATCH /db#
Updates the first matching row (limit 1). The updates array specifies which fields to change:
// Request body
{ "table": "tasks", "where": { "id": { "eq": "tk_abc123" } }, "updates": [{ "column": "isComplete", "value": true }] }
// Response
{ "data": { "id": "tk_abc123", "title": "Buy groceries", "isComplete": true, ... } }
update many — PATCH /db/many#
Same shape as update, but matches all rows satisfying where. Optional limit
caps how many rows are updated:
{
"table": "tasks",
"where": { "isComplete": { "eq": false } },
"updates": [{ "column": "isComplete", "value": true }]
}
Response: { "data": [...] } — array of all updated rows.
delete — DELETE /db#
Deletes the first matching row (limit 1). Returns 204 No Content.
{ "table": "tasks", "where": { "id": { "eq": "tk_abc123" } } }
delete many — DELETE /db/many#
Same shape as delete, but matches all rows satisfying where. Optional limit
caps deletions. Returns 204 No Content.
{ "table": "tasks", "where": { "isComplete": { "eq": true } } }
Auto-Managed Fields#
id— generated by Zonai on create; the client cannot set itcreatedAt— set to the current timestamp on create; ignored on updateupdatedAt— automatically updated to the current timestamp on every update-
.password— values are Argon2id-hashed before storage; the raw value is never stored or returned in responses -
.updatedWhenColumn— automatically updated to the current timestamp on every update of the watched column
Update Value Types#
The value field in an update entry supports special types for numeric and list columns:
{ "column": "viewCount", "value": { "increment": 1 } }
{ "column": "viewCount", "value": { "decrement": 1 } }
{ "column": "tags", "value": { "add": "dart" } }
{ "column": "tags", "value": { "remove": "dart" } }
{ "column": "description", "value": null }
Plain values set the column literally.
