L’Invariante
Il runtime edge è l’unico scrittore dei dati business. Nessun route handler, script o plugin scrive le tabelle biz_* direttamente — ogni mutazione passa da un trigger dichiarato nel manifest, che impacchetta RBAC, constraint, enforcement FSM, mapping data-link, audit ed emissione outbox in un’unica transazione.
Seconda invariante: i tenant non si vedono tra loro. La Row-Level Security di PostgreSQL limita ogni riga biz_*/ax_* al tenant_id della sessione — l’isolamento lo impone il database, non il codice che si ricorda una WHERE.
Il Contratto
Il manifest dichiara i motori — forma da manifests/LayoutManifest.production-line.example.json:
{
"action_engine": {
"transitions": [{ "from": "VERIFICATO", "to": "ACCETTATO", "trigger": "ACCEPT_LOT" }]
},
"data_link_engine": {
"queries": {
"ACCEPT_LOT": { "type": "INSERT", "table": "biz_lotti",
"mapping": { "lotto": "$payload.lot_id",
"operator_id": "$context.operatorId" } } }
},
"rbac_engine": { "permissions": { "MAGAZZINIERE": ["ACCEPT_LOT", "…"] } },
"constraint_engine": { "pre_commit_rules": [{ "id": "peso_coerente",
"expression": "$formula.weight_delta_pct <= 0.5 && $formula.weight_delta_pct >= -0.5",
"triggers": ["ACCEPT_LOT"] }] }
}
Contesto di sessione iniettato dal runtime — il payload non può sovrascriverlo:
interface EdgeContext {
tenantId: string; // dalla sessione, imposto dalla RLS
deviceCode: string; // $context.deviceCode — identità del device
userRole: string; // MAGAZZINIERE | OPERATORE | QUALITA | ADMIN | …
operatorId: string; // chi ha toccato davvero la macchina
}
Il Flusso
POST /api/v1/actions/:trigger
→ auth di sessione (ax_sessions, scadenza enforced)
→ check RBAC (role ∈ permissions) → 403 ACTION_FAILED
→ check constraint (pre_commit_rules) → 400 ACTION_FAILED
→ check FSM (from → to dichiarata) → 409 ACTION_FAILED
→ scrittura data-link + riga audit + outbox [stessa transazione]
→ 200 { committed, to_state, … }
Esempio di Payload
# Accetta un lotto — il constraint sul peso deve reggere o niente commit
curl -X POST http://edge:8291/api/v1/actions/ACCEPT_LOT \
-H "Authorization: Bearer $TOKEN" -H "X-Idempotency-Key: $(uuidgen)" \
-d '{"payload":{"lot_id":"LOT-001","peso_dichiarato":100.0,
"peso_misurato":100.3}}'
I Fallimenti Noti
| Sintomo | Causa → fix |
|---|---|
401 AUTH_REQUIRED |
TTL sessione scaduto o sessione revocata → rifai login |
409 ACTION_FAILED |
Transizione FSM non dichiarata dallo stato corrente → ispeziona le transitions del manifest |
403 ACTION_FAILED |
Il ruolo non ha il trigger → i permessi del manifest sono legge |
400 ACTION_FAILED |
Una pre_commit_rule è fallita → il messaggio d’errore nomina la regola |
LIST_* vuota che “dovrebbe avere righe” |
RLS: stai guardando un altro tenant/device di quanto credi |
| Scrittura ok ma il cloud non la vede | L’outbox drena in asincrono — guarda 02-cdc-outbox, non la tabella biz |