← Developers

03.3 — Query outbound & letture

Spec riferita a SPEC-DX-01 / api v1

L’Invariante

Le letture non toccano mai l’autorità del percorso di scrittura. Ogni risposta è calcolata dai fatti committati al momento della richiesta — le query SELECT del manifest sull’edge leggono le righe locali biz_* sotto RLS; /tenant/* sul Control Plane legge ciò che il canale di ingest firmato ha committato. Non c’è cache da invalidare né aggregato da fidarsi: la risposta che ricevi è lo stato dei fatti.

Il Contratto

Edge (sessione operatore): il data_link_engine del manifest dichiara query SELECT come trigger nominati — es. LIST_LOTTI nel verticale production-line. Passano per lo stesso endpoint /api/v1/actions/:trigger, stesso RBAC, stessa idempotenza — cambia solo la forma della query (type: "SELECT" restituisce rows invece di scrivere).

Cloud (sessione portale o device key full): /api/v1/tenant/* — snapshot usage, export audit hash-chained, registro audit-webhook, import-jobs, fatture, link al portale Stripe. Le proiezioni di business non sono hard-coded: sono quelle che il manifest del verticale dichiara.

Il Flusso

letture edge:   LIST_LOTTI → SELECT … FROM biz_lotti WHERE tenant=session   (RLS + SQL guard)
letture cloud:  /tenant/usage       → snapshot tenant + subscription + fleet + metering
                /tenant/audit       → export NDJSON a cursore di cp_admin_audit (since_seq)
                /tenant/audit/verify → ripercorre la hash chain → {ok, checked, head}

Esempio di Payload

# Edge — una query di lettura dichiarata, stessa famiglia di endpoint delle mutazioni
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 — la superficie badge: uno snapshot per contratto + flotta + 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}'

# Export SIEM — incrementale, paginato a cursore su 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"catena di audit rotta a {v.get('brokenAt')}"

I Fallimenti Noti

Sintomo Causa → rimedio
403 con token valido RBAC — il ruolo non ha il trigger di lettura in permissions, o binding CORS portal_origin (sessione del tenant A dall’origine del tenant B)
Risultato vuoto, dati “che esistono” Scope tenant_id/filtro sbagliato — le letture sono RLS-truthful, allarga il filtro
400 su since_seq Cursore malformato/negativo — l’exporter non riavvolge mai silenziosamente a 0
Dati cloud che sembrano stantii L’edge non ha ancora flushato — controlla il drain di sync/ingest e l’heartbeat last_poll_at del nodo
Endpoint verticale mancante Non dichiarato nel manifest — /tenant/* copre solo contratto/audit/billing; i dati di business sono dichiarati dal manifest