The Invariant
The edge runtime is the only writer of business data. No route handler, script or plugin writes biz_* tables directly — every mutation goes through a manifest-declared trigger, which bundles RBAC, constraint, FSM enforcement, data-link mapping, audit and outbox emission in a single transaction.
Second invariant: tenants cannot see each other. PostgreSQL Row-Level Security scopes every biz_*/ax_* row to the session’s tenant_id — isolation is enforced by the database, not by application code remembering a WHERE clause.
The Contract
A manifest declares the engines — shape from 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"] }] }
}
Session context injected by the runtime — payload cannot override it:
interface EdgeContext {
tenantId: string; // from session, enforced by RLS
deviceCode: string; // $context.deviceCode — device identity
userRole: string; // MAGAZZINIERE | OPERATORE | QUALITA | ADMIN | …
operatorId: string; // who actually touched the machine
}
The Flow
POST /api/v1/actions/:trigger
→ session auth (ax_sessions, expiry enforced)
→ RBAC check (role ∈ permissions) → 403 ACTION_FAILED
→ constraint check (pre_commit_rules) → 400 ACTION_FAILED
→ FSM check (from → to declared) → 409 ACTION_FAILED
→ data-link write + audit row + outbox [same transaction]
→ 200 { committed, to_state, … }
Payload Example
# Accept a lot — the weight constraint must hold or nothing commits
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}}'
Known Failures
| Symptom | Cause → fix |
|---|---|
401 AUTH_REQUIRED |
Session TTL lapsed or revoked → login again |
409 ACTION_FAILED |
FSM transition not declared from current state → inspect manifest transitions |
403 ACTION_FAILED |
Role lacks the trigger → manifest permissions are the law |
400 ACTION_FAILED |
pre_commit_rule failed → the error message names the rule |
Empty LIST_* result that “should have rows” |
RLS: you are looking at a different tenant/device than you think |
| Write works but cloud never sees it | Outbox drains asynchronously — check 02-cdc-outbox, not the biz table |