Order event schema
The order event is the heart of CounterFire. Every meaningful thing that happens to an order is recorded as an append-only event, and that stream of events is the single source of truth that both the kitchen display and the customer tracker read from. This page describes the event, how it is sequenced, and how it is versioned.
Where events come from
The canonical OrderEvent type lives in packages/core, the framework-agnostic
domain package that carries a hard 100 percent test coverage gate. apps/api
imports it, so the event shape is defined in exactly one tested place and reused
everywhere.
Events are persisted in the order_event table in D1. The table is
append-only: rows are never updated or deleted, so the full history of an
order can always be replayed.
The event record
Each persisted event row carries:
| Field | Meaning |
|---|---|
id | Primary key of the event. |
order_id | The order this event belongs to. |
restaurant_id | The owning restaurant. |
seq | Monotonic per-order sequence integer. See below. |
type | The event type, one of the values below. |
payload_json | The canonical OrderEvent payload from packages/core. |
created_at | When the event was appended. |
Event types
The order lifecycle is expressed as these event types:
| Event type | Marks the transition to | Customer tracker |
|---|---|---|
order_placed | placed | (order created) |
order_paid | paid | Received pipeline begins |
order_accepted | accepted | Received |
order_started | in kitchen | In Kitchen |
order_ready | ready | Ready |
order_completed | completed | (order closed) |
order_cancelled | cancelled | (order cancelled) |
order_paid is the pivotal event: it is what lights up the KDS, starts the
customer tracker, and triggers the receipt pipeline.
The order status enum these map onto is
placed / paid / accepted / in_kitchen / ready / completed / cancelled. The
allowed transitions between statuses are guarded by the order state machine in
packages/core; an event for a disallowed transition is rejected rather than
appended.
Sequence numbers (seq)
Every order has its own monotonically increasing seq, starting at the first
event for that order and incrementing by one per appended event. The sequence is
per order, not global.
seq is the backbone of correct realtime delivery:
- It defines a total order on an order’s events, so consumers can apply them in the exact order they happened.
- A client tracks the highest
seqit has applied. On reconnect it acknowledges that value and receives only events with a higherseq, so it replays just the gap. - A client merges events keyed by
(order_id, seq). An event whose(order_id, seq)has already been applied is ignored, which makes delivery idempotent and duplicates impossible.
This trio (append-only log, per-order seq, idempotent merge) is the mechanism
behind the no-dropped, no-duplicated, ordered-delivery guarantee. See
realtime: reconnect and snapshots.
Versioning
The event payload carries a version so consumers can evolve safely. Events are written once and replayed potentially much later, so the payload schema is versioned rather than mutated in place. New event payload versions are additive and old persisted events remain readable, because the log is never rewritten.
The fixed receipt model is similarly pinned to a fixed format version (v1 in
V1), so a receipt rendered from an order’s events is reproducible.
Why this design
Two related risks drove it:
- Realtime correctness on reconnect is the make-or-break property of the
product. An append-only log with a monotonic
seqand idempotent client merge is what makes exactly-once, ordered delivery achievable. - The realtime coordinator must not become the source of truth. The durable event log in D1 is authoritative; the realtime fan-out rehydrates from it. An eviction or restart of the realtime layer therefore cannot lose tickets, because the truth is the persisted log.