The Invariant
A committed business write must never be lost between the local commit and cloud delivery. The kernel writes the business row, the audit row and the outbox entry in the same transaction — if the row exists, the event will eventually reach the cloud. The sync channel is asynchronous by design: edge autonomy first, cloud convergence eventually.
The Contract
interface OutboxEntry { // ax_cdc_outbox (edge-side)
id: string; // uuid — becomes the global dedup key
aggregate_type: string; // BIZ_*
aggregate_id: string;
event_type: string; // INSERT_* | UPDATE_*
payload: object;
created_at: string; // UTC
}
// Cloud side: cp_events stores it verbatim; cp_event_dedup(id) is the
// exactly-once frontier — processed ids are never re-processed.
Channel limits (hard contract): ingest batches ≤ 1000 events (413 BATCH_TOO_LARGE), edge-audit NDJSON ≤ 5000 lines. 503 code: DRAINING means the cloud is shutting down — back off, do not alert.
The Flow
txn: biz row + audit + outbox ──commit──► daemon polls ax_cdc_outbox
│ batch (≤1000), ordered by seq
▼
POST /sync/ingest (Bearer edge api_key)
│ dedup on id
▼
cp_events ──► projections on read
│
ack → edge marks entries delivered
cloud down? entries stay queued, edge keeps producing — offline-first
Every authenticated sync call also touches last_poll_at — the fleet heartbeat is a free side-effect of delivery.
Payload Example
# The daemon's drain — exactly what it sends, nothing more
curl -X POST https://cp.axiom-trace.eu/api/v1/sync/ingest \
-H "Authorization: Bearer $EDGE_API_KEY" -H 'content-type: application/json' \
-d '{"events":[{"id":"…","aggregate_type":"BIZ_CYCLES",
"aggregate_id":"…","event_type":"INSERT_BIZ_CYCLES",
"payload":{"cycle_seq":1042,"duration_ms":812},
"created_at":"2026-09-26T14:01:03Z"}]}'
Known Failures
| Symptom | Cause → fix |
|---|---|
401 on ingest |
API key rotated at re-claim; old key invalid by design → re-provision |
403 EDGE_CAP_EXCEEDED |
Tenant over licensed edge count → admin must raise max_edges |
413 BATCH_TOO_LARGE |
>1000 events → split; never retry the same oversized batch |
429 |
Per-tenant ingest bucket → honor Retry-After, jittered backoff |
503 DRAINING |
Cloud restart → retry later; alerting on this is a false positive |
| Events stuck in outbox | Check daemon logs and network — the rows are safe, delivery is deferred |