dabadataDocs
Concepts / Trace

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:

FieldTypeMeaning
idnumberIncreasing id. Use it to page backwards.
atISO stringWhen the call happened.
sourcehttp | ws | mcpHow the call arrived. Subscriptions are ws. The MCP server talks HTTP, so its calls show http for now.
userobject{ id, role, name?, issuer? }. The project key appears as service, a view token as the viewer, no token as anonymous.
opstringinsert, 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).
tablestringThe table.
rowIdstring | nullThe row, for mutations and single-row reads.
decisionallowed | denied | bypassWhat the rules decided.
ruleRule | nullThe rule that was applied, as JSON.
explanationstring | nullThe sentence naming the path through the rule.
valuesobject | nullThe values of a mutation. For an insert, after defaults and user ids were filled in.
beforeobject | nullThe row as it was when the rule looked at it (update, delete) - recorded even when the call was denied.
afterobject | nullThe row after the change (insert, update).
rowCountnumber | nullHow many rows a read or subscription returned.
resultok | denied | errorHow the call ended.
errorstring | nullThe error message when result is error, e.g. a validation failure.
durationMsnumber | nullTime spent in the server.
fanoutnumberHow 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:

json
{
  "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:

bash
curl "https://dabadata.com/v1/trace?limit=50&table=tasks&user=carol" \
  -H "Authorization: Bearer <project key>"
ParameterDefaultMeaning
limit200At most 1000.
beforenoneOnly rows with a smaller id. Pass the last id you have to get the next page.
tablenoneOnly this table.
usernoneOnly 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 user first. Most questions are "why could this person not ...".
  • A row with result: "error" and no decision never reached the rules: the request itself was wrong (an unknown column, a value of the wrong type, a missing row). error says what.
  • Compare before and after to see exactly what an update changed.
  • bypass rows 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:

json
"trace": { "read": { "role": "admin" } }
  • read is a rule like a table's, checked against the caller (owner has no row here and is an error). A user token, or the publishable key alone (as anonymous), that passes it reads GET /v1/trace for 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 forbidden with the explanation. Without a trace block only the secret key, view tokens and the console read it (401 for anyone else), as before.
  • Live: a /v1/live socket 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.
ts
// 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.

dabadata documentation, written against API contract v1.