LogoZonai

Default Operations

The built-in CRUD operations every table gets without any code.

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#

OperationMethodPathBody 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>
createPOST/dbJSON body
create many POST /db/many JSON body
updatePATCH/dbJSON body
update many PATCH /db/many JSON body
deleteDELETE/dbJSON 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 it
  • createdAt — set to the current timestamp on create; ignored on update
  • updatedAt — 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.