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 |