Concepts
Trace
dabadata keeps a trace: every mutation, every read through /v1/db and every live subscription writes one row to the trace: who did it, to which table, which rule applied, what was decided and why. When something surprises you, the answer is already written down.
What is recorded
Trace rows are stored in Postgres and returned as JSON with camelCase keys:
| Field | Type | Meaning |
|---|---|---|
id | number | Increasing id. Use it to page backwards. |
at | ISO string | When the call happened. |
source | http | ws | mcp | How the call arrived. Subscriptions are ws. The MCP server talks HTTP, so its calls show http for now. |
user | object | { id, role, name?, issuer? }. The project key appears as service, a view token as the viewer, no token as anonymous. |
op | string | insert, update, delete, read or subscribe; also, among others, check (a transaction's check), action (an action's own row) and ai (an action's model call). |
table | string | The table. |
rowId | string | null | The row, for mutations and single-row reads. |
decision | allowed | denied | bypass | What the rules decided. |
rule | Rule | null | The rule that was applied, as JSON. |
explanation | string | null | The sentence naming the path through the rule. |
values | object | null | The values of a mutation. For an insert, after defaults and user ids were filled in. |
before | object | null | The row as it was when the rule looked at it (update, delete) - recorded even when the call was denied. |
after | object | null | The row after the change (insert, update). |
rowCount | number | null | How many rows a read or subscription returned. |
result | ok | denied | error | How the call ended. |
error | string | null | The error message when result is error, e.g. a validation failure. |
durationMs | number | null | Time spent in the server. |
fanout | number | How many live subscriptions received the change. |
Not traced: /v1/health, /v1/schema, token minting, and the simulator - a simulation changes nothing, so it leaves no row.
An example row
Carol, a viewer, tried to delete a task:
{
"id": 39,
"at": "2026-10-15T14:02:11.204Z",
"source": "http",
"user": { "id": "carol", "role": "viewer", "name": "Carol" },
"op": "delete",
"table": "tasks",
"rowId": "5f1c2a9e-8b1d-4c2e-9a77-3d0e6b2f4a10",
"decision": "denied",
"rule": { "role": "admin" },
"explanation": "delete tasks: denied, role \"viewer\" not in [\"admin\"]",
"values": null,
"before": {
"id": "5f1c2a9e-8b1d-4c2e-9a77-3d0e6b2f4a10",
"title": "Order chairs",
"done": false,
"owner": "bob",
"due": null,
"notes": null,
"created_at": "2026-10-15T13:40:02.118Z",
"updated_at": "2026-10-15T13:40:02.118Z",
"created_by": "bob"
},
"after": null,
"rowCount": null,
"result": "denied",
"error": null,
"durationMs": 3,
"fanout": 0
}The same explanation came back to Carol in the 403 body, together with "traceId": 39, so a support question can start from the exact row.
Fan-out
When a mutation succeeds, the server sends the change to every live subscription that may see the row, then records how many that was in fanout. Zero means nobody was listening (or nobody listening may read it). Reads and subscriptions always have 0. It is the quickest way to answer "did the other screen get it?". See Live subscriptions.
Reading the trace
GET /v1/trace with the project key or a view token returns rows newest first:
curl "https://dabadata.com/v1/trace?limit=50&table=tasks&user=carol" \
-H "Authorization: Bearer <project key>"| Parameter | Default | Meaning |
|---|---|---|
limit | 200 | At most 1000. |
before | none | Only rows with a smaller id. Pass the last id you have to get the next page. |
table | none | Only this table. |
user | none | Only this user id. |
To follow it live, open /v1/live with a view token: every new row arrives as { "type": "trace", "trace": { ... } }.
A few habits that help when reading it:
- Filter by
userfirst. Most questions are "why could this person not ...". - A row with
result: "error"and nodecisionnever reached the rules: the request itself was wrong (an unknown column, a value of the wrong type, a missing row).errorsays what. - Compare
beforeandafterto see exactly what an update changed. bypassrows were made with the project key or a view token. If you see more of them than you expect, find out who holds the key.
Let your app's admins read it (trace.read)
The trace is the "recent activity" an admin page wants. Without a backend of your own, open it to some of your users with a rule at the top level of the definition, next to tables:
"trace": { "read": { "role": "admin" } }readis a rule like a table's, checked against the caller (ownerhas no row here and is an error). A user token, or the publishable key alone (asanonymous), that passes it readsGET /v1/tracefor its environment, with the same parameters, values included: whoever passes sees what every write wrote. Keep it to people who may.- A caller the rule refuses gets 403
forbiddenwith the explanation. Without atraceblock only the secret key, view tokens and the console read it (401 for anyone else), as before. - Live: a
/v1/livesocket whose user passes the rule gets the{ "type": "trace", "trace": { ... } }messages too. The rule is checked for every message against the current definition, with the identity the socket opened with.
// The admin page's activity feed: the newest 50 writes.
const res = await fetch("https://dabadata.com/v1/trace?limit=50", {
headers: { "X-Api-Key": "pk_live_EXAMPLEexample0000000000", Authorization: `Bearer ${db.token}` },
});
const { trace } = await res.json();
for (const t of trace.filter((t) => t.op !== "read" && t.op !== "subscribe")) {
console.log(t.at, t.user?.id, t.op, t.table, t.result);
}The viewer's Trace page
Open the viewer at /?token=<view token> and choose Trace. It lists the newest calls first and prepends new ones as they happen, with a short highlight. You can narrow it by table, user and decision, and open any row to see the rule, the values, and the row before and after. The Tables page shows the rows themselves, live; Rules holds the matrix and the simulator.
See it in the demo
The live demo runs the shared task list in two panes, each as a different person, with the trace streaming underneath. Make Carol try to delete something and the denial, with its sentence, shows up below the moment she clicks.