Skip to content

Kitchen Order Management

1. Overview

The Kitchen Order Management system bridges the front-of-house (FOH) POS terminal and the back-of-house (BOH) kitchen display system (KDS). When a waiter sends items to the kitchen, a KitchenTicket is created containing one or more KitchenTicketItems. The kitchen staff progresses items through cooking stages, and the ticket status auto-progresses based on item statuses.

Source: Entity schemas in @nx/core (packages/core/src/models/schemas/sale/kitchen-ticket/, kitchen-ticket-item/).

Design Principles

PrincipleDescription
Void and re-sendOnce sent, ticket items cannot be modified in-place. All changes (qty, modifiers, replacement) follow the void + re-send pattern — void the old ticket/item, then send a new one with the correct data
Auto-progressionTicket status is derived from item statuses automatically via evaluateTicketAutoProgression — no manual ticket status management needed
Idempotent sendsOptional idempotencyKey prevents duplicate tickets on retry

Industry Context

The void-and-resend approach is the industry standard across major KDS platforms. During the design of this system, we researched and compared against 6 commercial KDS platforms:

SystemMarketKey Similarity
Toast POSUS market leader (127K+ restaurants)Cloud-based, auto-progression from station to serving
Square KDSSMB-focusedCannot modify after send — void and re-enter
Lightspeed RestaurantMid-market, globalCannot modify — void and re-enter
Oracle MICROS SimphonyEnterprise chainsKDS Controller for centralized coordination
Aloha POS (NCR Voyix)Full-service restaurantsForced void acknowledgment on KDS, void and re-enter
Odoo POSOpen-source, ERP-integratedManual stage progression

Key findings:

  • 5 out of 6 systems use void-and-resend for qty/modifier changes (only Toast supports real-time in-place modification)
  • Our 5 item statuses (PENDING → COOKING → READY → SERVED + VOIDED) are more granular than the typical 3–4 states, giving better tracking
  • Our evaluateTicketAutoProgression is more sophisticated than most systems' station-to-serving auto-progression
  • Our transaction-based coordination is equivalent to Oracle's KDS Controller or Aloha's KDS Master architecture

2. Entity Relationship Diagram

3. Status Lifecycle

3.1. KitchenTicketItem Statuses

StatusInternal ValueTransition FromTransition To
PENDING103_PENDINGCreatedCOOKING, VOIDED
COOKING203_PROCESSINGPENDINGREADY, VOIDED
READY302_SUCCESSCOOKINGSERVED, VOIDED
SERVED303_COMPLETEDREADYTerminal
VOIDED505_CANCELLEDPENDING, COOKING, READYTerminal

3.2. KitchenTicket Statuses (Auto-Progressed)

Ticket status is never set directly — it is computed by evaluateTicketAutoProgression based on the aggregate state of all items:

StatusInternal ValueTrigger Condition
PENDING103_PENDINGCreated, no items started
PROCESSING203_PROCESSINGAny item is COOKING, READY, or SERVED
READY302_SUCCESSAll active items are READY, SERVED, or VOIDED
COMPLETED303_COMPLETEDAll items terminal, at least 1 SERVED
VOIDED505_CANCELLEDALL items VOIDED, none SERVED

IMPORTANT

A ticket with mixed SERVED + VOIDED items becomes COMPLETED, not VOIDED. VOIDED status only applies when every item is voided.

4. REST API

Base path: /v1/api/sale

4.1. FOH Endpoints (POS Terminal)

Send to Kitchen

Creates a new kitchen ticket with items from a sale order.

POST /sale-orders/{saleOrderId}/kitchen-tickets/send
FieldTypeRequiredDescription
itemsArray<{ saleOrderItemId, quantity }>YesItems to send (quantity as string)
kitchenStationIdstringNoTarget kitchen station
prioritynumberNo0=normal (default), 1=rush
notestringNoNote displayed on KDS (max 500 chars)
idempotencyKeystringNoPrevents duplicate tickets on retry (max 100 chars)

Response: KitchenTicket with items array.

Idempotency: If idempotencyKey matches an existing ticket for the same sale order, the existing ticket is returned without creating a new one.

Void Ticket

Voids all non-terminal items in a ticket. SERVED items are untouched.

POST /kitchen-tickets/{ticketId}/void
FieldTypeRequiredDescription
reasonstringNoVoid reason (max 500 chars)

Response: KitchenTicket — status is VOIDED if all items voided, COMPLETED if some were SERVED.

Rush Ticket

Marks a ticket as high priority.

POST /kitchen-tickets/{ticketId}/rush
FieldTypeRequiredDescription
reasonstringNoRush reason (max 500 chars)

Response: KitchenTicket with priority: 1 and metadata.rushReason.

Void Single Item

POST /kitchen-ticket-items/{itemId}/void
FieldTypeRequiredDescription
reasonstringNoVoid reason

Response: KitchenTicketItem with status VOIDED. Auto-progresses parent ticket.

4.2. BOH Endpoints (KDS)

Item-Level Status Transitions

POST /kitchen-ticket-items/{itemId}/start-cooking   # PENDING → COOKING
POST /kitchen-ticket-items/{itemId}/ready            # COOKING → READY
POST /kitchen-ticket-items/{itemId}/served           # READY → SERVED

No request body required. Each returns the updated KitchenTicketItem and auto-progresses the parent ticket.

Bulk Ticket Operations

POST /kitchen-tickets/{ticketId}/ready    # All PENDING/COOKING items → READY
POST /kitchen-tickets/{ticketId}/served   # All READY items → SERVED

No request body required. Returns updated KitchenTicket with items.

4.3. Read Endpoints

MethodPathDescription
GET/kitchen-ticketsList tickets (filter, include, paginate)
GET/kitchen-tickets/{id}Get ticket by ID
GET/kitchen-ticket-itemsList items (filter, paginate)
GET/kitchen-ticket-items/{id}Get item by ID

Common filters:

GET /kitchen-tickets?filter[saleOrderId]=xxx&include=items
GET /kitchen-tickets?filter[kitchenStationId]=xxx&filter[status][$ne]=505_CANCELLED
GET /kitchen-ticket-items?filter[kitchenTicketId]=xxx

4.4. Endpoint Summary

FOH has only 3 write actions for tickets:

ActionWhat it does
SendCreate a new ticket with items
Void TicketCancel all active items in a ticket
Void ItemCancel a single item

Everything else (qty change, modifier change, replace, re-fire) is a combination of void + send. This is the same approach used by Square, Lightspeed, Aloha, and Oracle MICROS.

5. WebSocket Events

5.1. Topics

TopicValue
Kitchen Ticketwt:observation/sale/kitchen-ticket
Kitchen Ticket Itemwt:observation/sale/kitchen-ticket-item

5.2. Rooms

Events are emitted to multiple rooms simultaneously:

RoomPatternSubscriber
Merchant (all)wr:observation/merchants/{merchantId}Dashboard
Merchant Kitchenwr:observation/merchants/{merchantId}/kitchenAll KDS screens
Order Kitchenwr:observation/sale-orders/{saleOrderId}/kitchenPOS order detail
Stationwr:observation/kitchen-stations/{stationId}Station-specific KDS
Ticketwr:observation/kitchen-tickets/{ticketId}Ticket detail
Ticket Itemswr:observation/kitchen-tickets/{ticketId}/itemsItem-level events
Specific Itemwr:observation/kitchen-ticket-items/{itemId}Single item

5.3. Event Actions

ActionTriggerPayload
TICKET_CREATEDsendToKitchen{ ticket, items[], merchantId }
TICKET_VOIDEDvoidTicket{ ticket, items[], merchantId }
TICKET_RUSHEDrushTicket{ ticket, items[], merchantId }
TICKET_STATUS_CHANGEDmarkTicketReady, markTicketServed{ ticket, items[], merchantId }
ITEM_VOIDEDvoidTicketItem{ ticket, item, merchantId }
ITEM_STATUS_CHANGEDstartCookingItem, markItemReady, markItemServed{ ticket, item, merchantId }

6. Services

6.1. KitchenTicketService

Source: src/services/kitchen-ticket.service.tsDI Dependencies: SaleOrderRepository, SaleOrderItemRepository, KitchenStationRepository, KitchenTicketRepository, KitchenTicketItemRepository, SaleSocketEventService

MethodDescription
sendToKitchenValidates order + items + station, creates ticket + items in transaction. Supports idempotency key to prevent duplicate tickets.
voidTicketVoids all active items (PENDING/COOKING/READY) in single SQL update. SERVED items untouched. Auto-progresses ticket. Stores void reason in metadata.
rushTicketSets priority=1 and stores rush reason. Non-transactional (simple update).
markTicketReadyBulk updates all PENDING/COOKING items to READY. Auto-progresses ticket.
markTicketServedBulk updates all READY items to SERVED. Auto-progresses ticket.

6.2. KitchenTicketItemService

Source: src/services/kitchen-ticket-item.service.tsDI Dependencies: KitchenTicketRepository, KitchenTicketItemRepository, SaleSocketEventService

MethodDescription
voidTicketItemVoids single item (guards canVoid). Auto-progresses parent ticket.
startCookingItemPENDING → COOKING. Sets startedAt. Auto-progresses ticket to PROCESSING.
markItemReadyCOOKING → READY. Sets readyAt. May auto-progress ticket to READY.
markItemServedREADY → SERVED. Sets servedAt. May auto-progress ticket to COMPLETED.

All item transitions use a shared _transitionItemStatus helper that validates the guard, updates the item, and calls evaluateTicketAutoProgression.

7. Repositories

RepositoryEntityCustom Methods
KitchenTicketRepositoryKitchenTicketgetNextSequence, findByIdempotencyKey, evaluateTicketAutoProgression
KitchenTicketItemRepositoryKitchenTicketItemfindByTicketId, findBySaleOrderItemId, findActiveByTicketId
KitchenStationRepositoryKitchenStationStandard CRUD

evaluateTicketAutoProgression

Source: packages/core/src/repositories/sale/kitchen-ticket.repository.ts

Runs 4 conditional SQL updates in priority order (all within the provided transaction):

  1. All items VOIDED → ticket VOIDED
  2. All SERVED/VOIDED (1+ SERVED) → ticket COMPLETED
  3. All READY/SERVED/VOIDED → ticket READY
  4. Any COOKING/READY/SERVED → ticket PROCESSING
  5. Otherwise → no change (remains PENDING)

Each step uses EXISTS/NOT EXISTS subqueries to avoid race conditions.

8. Permissions

CodeActionDescription
KitchenTicket.sendCREATESend items to kitchen
KitchenTicket.voidDELETEVoid a kitchen ticket
KitchenTicket.rushUPDATEMark ticket as rush
KitchenTicket.updateTicketStatusUPDATEBulk status updates (KDS)
KitchenTicketItem.voidItemDELETEVoid single item
KitchenTicketItem.updateItemStatusUPDATEItem status transitions (KDS)

9. Operational Scenarios

9.1. Normal Order Flow

Waiter: Create SaleOrder → Add items
Waiter: POST /sale-orders/{id}/kitchen-tickets/send
  → Ticket PENDING, all items PENDING

Chef:   POST /kitchen-ticket-items/{id}/start-cooking  (per item)
  → Item COOKING, ticket auto → PROCESSING

Chef:   POST /kitchen-ticket-items/{id}/ready  (per item)
  → Item READY, when all ready → ticket auto → READY

Waiter: POST /kitchen-ticket-items/{id}/served  (per item)
  → Item SERVED, when all served → ticket auto → COMPLETED

9.2. Partial Send (Batches)

Waiter: Send appetizers → Ticket #1 (sequence: 1)
  ...guest finishes...
Waiter: Send mains      → Ticket #2 (sequence: 2)

Each sendToKitchen call creates a new ticket. The sequence field auto-increments per SaleOrder.

9.3. Quantity Change After Send (Void + Re-send)

All quantity changes follow the same void + re-send pattern:

Guest:  "Change 3x Burger to 1x Burger"

Option A — Void entire ticket and re-send:
  Waiter: POST /kitchen-tickets/{ticket1Id}/void
          { "reason": "Qty change — guest wants 1 burger" }
  Waiter: POST /sale-orders/{id}/kitchen-tickets/send
          { "items": [{ "saleOrderItemId": "burger-id", "quantity": "1" }] }

Option B — Void individual items (if ticket has other items to keep):
  Waiter: POST /kitchen-ticket-items/{burgerItemId}/void
          { "reason": "Qty change" }
  Waiter: POST /sale-orders/{id}/kitchen-tickets/send
          { "items": [{ "saleOrderItemId": "burger-id", "quantity": "1" }] }

TIP

FE should check item status before voiding. If items are COOKING or READY, warn the user ("This item is being prepared. Void anyway?"). SERVED items cannot be voided.

9.4. Item Replacement

Waiter: POST /kitchen-ticket-items/{burgerId}/void  { reason: "Swap to chicken" }
Waiter: POST /sale-orders/{id}/kitchen-tickets/send  { items: [chicken] }

9.5. Re-fire (Remake)

Waiter: POST /kitchen-ticket-items/{originalId}/void  { reason: "Dropped plate" }
Waiter: POST /sale-orders/{id}/kitchen-tickets/send
        { items: [same item], priority: 1, note: "RE-FIRE — dropped plate" }

Set priority: 1 to mark as rush. The note field appears on KDS.

9.6. Void Entire Ticket

Waiter: POST /kitchen-tickets/{ticketId}/void  { reason: "Table cancelled" }

System: All PENDING/COOKING/READY items → VOIDED
        SERVED items untouched
        If all VOIDED → ticket VOIDED
        If mixed SERVED+VOIDED → ticket COMPLETED

9.7. Decision Tree

Waiter wants to change something after send?

├── Change qty (increase or decrease)?
│   └── Void ticket → re-send with new qty
│       (or void individual items if ticket has other items to keep)

├── Delete item entirely?
│   └── Void item (or void ticket if it's the only item)

├── Replace item (swap)?
│   └── Void old item → send new item

├── Change modifiers?
│   └── Void old item → send new item with updated modifiers

├── Re-fire (remake)?
│   └── Void old item → send new item with priority: 1

├── Rush?
│   └── POST /kitchen-tickets/{id}/rush

└── Cancel everything?
    └── POST /kitchen-tickets/{id}/void

10. Frontend Integration

10.1. POS Terminal (FOH)

ResponsibilityImplementation
Send unsent items to kitchenPOST /sale-orders/{id}/kitchen-tickets/send
Void items/tickets on guest cancellationPOST /kitchen-ticket-items/{id}/void or /kitchen-tickets/{id}/void
Qty/modifier/replacement changesVoid old ticket or item → re-send with correct data
Rush a ticketPOST /kitchen-tickets/{id}/rush
Track kitchen status per orderSubscribe to wr:observation/sale-orders/{saleOrderId}/kitchen
Idempotency on retryInclude idempotencyKey in send requests
Pre-void validationCheck item status before void — warn if COOKING/READY, block if SERVED

10.2. KDS Screen (BOH)

ResponsibilityImplementation
List active tickets for stationGET /kitchen-tickets?filter[kitchenStationId]={id}&filter[status][$ne]=505_CANCELLED
Progress items through stagesPOST /kitchen-ticket-items/{id}/start-cooking, /ready, /served
Bulk complete ticketPOST /kitchen-tickets/{id}/ready or /served
Display rush indicatorCheck ticket.priority === 1
Display void indicatorFlash/highlight voided items for chef acknowledgment
Sort by priority + timeOrder by priority DESC, pendingAt ASC
Real-time updatesSubscribe to wr:observation/kitchen-stations/{stationId}

10.3. Concurrency Handling

MechanismDescription
Idempotency keyInclude idempotencyKey in send requests (e.g., {saleOrderId}-{timestamp}-{random}) — prevents duplicate tickets on retry
SaleOrderItem validationsendToKitchen validates each saleOrderItemId exists — if another waiter deleted the item, request fails with 404
Status guard on voidVoiding an already-VOIDED or SERVED item returns 400 — FE should handle gracefully and refresh state from WS
WS-driven stateOn WS events, refresh ticket/item state rather than relying on local state

11. Data Model Reference

KitchenTicket

FieldTypeDescription
idstringSnowflake ID
ticketNumberstringUnique identifier (timestamp-snowflake)
saleOrderIdstringParent sale order
merchantIdstringMerchant owner
kitchenStationIdstring?Assigned station (nullable)
statusstringCurrent status (auto-progressed)
prioritynumber0=normal, 1=rush
sequencenumberOrder within SaleOrder
pendingAtDateWhen created
processingAtDate?When first item started cooking
readyAtDate?When all items ready
completedAtDate?When all items served
voidedAtDate?When voided
metadataobject{ note?, idempotencyKey?, rushReason?, voidReason? }

KitchenTicketItem

FieldTypeDescription
idstringSnowflake ID
kitchenTicketIdstringParent ticket
saleOrderItemIdstringLinked sale order item
quantitystringQuantity (decimal as string)
statusstringCurrent status
startedAtDate?When cooking started
readyAtDate?When marked ready
servedAtDate?When served to guest
voidedAtDate?When voided
metadataobject{ productMetadata?, modifiers?, specialInstructions?, voidReason? }
DocumentDescription
Sale OrderSaleOrder entity, item modes, status lifecycle
Real-Time EventsWebSocket infrastructure, Signal integration
Checkout FlowOrder checkout and payment flow

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.