Skip to content

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:

FieldMeaning
idPrimary key of the event.
order_idThe order this event belongs to.
restaurant_idThe owning restaurant.
seqMonotonic per-order sequence integer. See below.
typeThe event type, one of the values below.
payload_jsonThe canonical OrderEvent payload from packages/core.
created_atWhen the event was appended.

Event types

The order lifecycle is expressed as these event types:

Event typeMarks the transition toCustomer tracker
order_placedplaced(order created)
order_paidpaidReceived pipeline begins
order_acceptedacceptedReceived
order_startedin kitchenIn Kitchen
order_readyreadyReady
order_completedcompleted(order closed)
order_cancelledcancelled(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 seq it has applied. On reconnect it acknowledges that value and receives only events with a higher seq, 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 seq and 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.