← Developers

03.3 — Outbound queries & reads

Spec matching SPEC-DX-01 / api v1

The Invariant

Reads never touch the write path’s authority. Every answer is computed from committed facts at request time — manifest SELECT queries on the edge read local biz_* rows under RLS; /tenant/* on the Control Plane reads what the signed ingest channel committed. There is no cache to invalidate and no aggregate to trust: the answer you get is the state of the facts.

The Contract

Edge (operator session): the manifest data_link_engine declares SELECT queries as named triggers — e.g. LIST_LOTTI in the production-line vertical. They go through the same /api/v1/actions/:trigger endpoint, same RBAC, same idempotency — only the query shape differs (type: "SELECT" returns rows instead of writing).

Cloud (portal session or full device key): /api/v1/tenant/* — usage snapshot, hash-chained audit export, audit-webhook registry, import-jobs, invoices, Stripe portal link. Business projections are not hard-coded: they are whatever the vertical manifest declares.

The Flow

edge reads:   LIST_LOTTI → SELECT … FROM biz_lotti WHERE tenant=session    (RLS + SQL guard)
cloud reads:  /tenant/usage       → tenant + subscription + fleet + metering snapshot
              /tenant/audit       → NDJSON cursor export of cp_admin_audit (since_seq)
              /tenant/audit/verify → re-walk the hash chain → {ok, checked, head}

Payload Example

# Edge — a declared read query, same endpoint family as mutations
curl -X POST http://localhost:8291/api/v1/actions/LIST_LOTTI \
  -H "Authorization: Bearer $TOKEN" -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{"payload":{"lot_id":"LOT-2026-091"}}' | jq '.rows'

# Cloud — the badge surface: one snapshot for contract + fleet + metering
curl -s https://cp.axiom-trace.eu/api/v1/tenant/usage -H "Authorization: Bearer $PS" \
  | jq '{plan: .subscription.plan, edges: .edges|length, events_month: .monthly_events}'

# SIEM export — incremental, cursor-paginated on seq
curl -s "https://cp.axiom-trace.eu/api/v1/tenant/audit?since_seq=$LAST&limit=1000" \
  -H "Authorization: Bearer $PS"
import httpx
c = httpx.Client(base_url="https://cp.axiom-trace.eu",
                 headers={"authorization": f"Bearer {token}"})
v = c.get("/api/v1/tenant/audit/verify").json()
assert v["ok"], f"audit chain broken at {v.get('brokenAt')}"

Known Failures

Symptom Cause → fix
403 with valid token RBAC — the role lacks the read trigger in permissions, or CORS portal_origin binding (tenant A’s session from tenant B’s origin)
Empty result, data “exists” Wrong tenant_id/filter scope — reads are RLS-truthful, widen the filter
400 on since_seq Malformed/negative cursor — the exporter never silently rewinds to 0
Cloud data looks stale The edge hasn’t flushed yet — check sync/ingest drain and the node’s last_poll_at heartbeat
Missing vertical endpoint Not declared in the manifest — /tenant/* only covers contract/audit/billing; business data is manifest-declared