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
| Principle | Description |
|---|---|
| Void and re-send | Once 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-progression | Ticket status is derived from item statuses automatically via evaluateTicketAutoProgression — no manual ticket status management needed |
| Idempotent sends | Optional 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:
| System | Market | Key Similarity |
|---|---|---|
| Toast POS | US market leader (127K+ restaurants) | Cloud-based, auto-progression from station to serving |
| Square KDS | SMB-focused | Cannot modify after send — void and re-enter |
| Lightspeed Restaurant | Mid-market, global | Cannot modify — void and re-enter |
| Oracle MICROS Simphony | Enterprise chains | KDS Controller for centralized coordination |
| Aloha POS (NCR Voyix) | Full-service restaurants | Forced void acknowledgment on KDS, void and re-enter |
| Odoo POS | Open-source, ERP-integrated | Manual 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
evaluateTicketAutoProgressionis 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
| Status | Internal Value | Transition From | Transition To |
|---|---|---|---|
| PENDING | 103_PENDING | Created | COOKING, VOIDED |
| COOKING | 203_PROCESSING | PENDING | READY, VOIDED |
| READY | 302_SUCCESS | COOKING | SERVED, VOIDED |
| SERVED | 303_COMPLETED | READY | Terminal |
| VOIDED | 505_CANCELLED | PENDING, COOKING, READY | Terminal |
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:
| Status | Internal Value | Trigger Condition |
|---|---|---|
| PENDING | 103_PENDING | Created, no items started |
| PROCESSING | 203_PROCESSING | Any item is COOKING, READY, or SERVED |
| READY | 302_SUCCESS | All active items are READY, SERVED, or VOIDED |
| COMPLETED | 303_COMPLETED | All items terminal, at least 1 SERVED |
| VOIDED | 505_CANCELLED | ALL 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| Field | Type | Required | Description |
|---|---|---|---|
items | Array<{ saleOrderItemId, quantity }> | Yes | Items to send (quantity as string) |
kitchenStationId | string | No | Target kitchen station |
priority | number | No | 0=normal (default), 1=rush |
note | string | No | Note displayed on KDS (max 500 chars) |
idempotencyKey | string | No | Prevents 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| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Void 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| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Rush reason (max 500 chars) |
Response: KitchenTicket with priority: 1 and metadata.rushReason.
Void Single Item
POST /kitchen-ticket-items/{itemId}/void| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Void 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 → SERVEDNo 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 → SERVEDNo request body required. Returns updated KitchenTicket with items.
4.3. Read Endpoints
| Method | Path | Description |
|---|---|---|
GET | /kitchen-tickets | List tickets (filter, include, paginate) |
GET | /kitchen-tickets/{id} | Get ticket by ID |
GET | /kitchen-ticket-items | List 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]=xxx4.4. Endpoint Summary
FOH has only 3 write actions for tickets:
| Action | What it does |
|---|---|
| Send | Create a new ticket with items |
| Void Ticket | Cancel all active items in a ticket |
| Void Item | Cancel 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
| Topic | Value |
|---|---|
| Kitchen Ticket | wt:observation/sale/kitchen-ticket |
| Kitchen Ticket Item | wt:observation/sale/kitchen-ticket-item |
5.2. Rooms
Events are emitted to multiple rooms simultaneously:
| Room | Pattern | Subscriber |
|---|---|---|
| Merchant (all) | wr:observation/merchants/{merchantId} | Dashboard |
| Merchant Kitchen | wr:observation/merchants/{merchantId}/kitchen | All KDS screens |
| Order Kitchen | wr:observation/sale-orders/{saleOrderId}/kitchen | POS order detail |
| Station | wr:observation/kitchen-stations/{stationId} | Station-specific KDS |
| Ticket | wr:observation/kitchen-tickets/{ticketId} | Ticket detail |
| Ticket Items | wr:observation/kitchen-tickets/{ticketId}/items | Item-level events |
| Specific Item | wr:observation/kitchen-ticket-items/{itemId} | Single item |
5.3. Event Actions
| Action | Trigger | Payload |
|---|---|---|
TICKET_CREATED | sendToKitchen | { ticket, items[], merchantId } |
TICKET_VOIDED | voidTicket | { ticket, items[], merchantId } |
TICKET_RUSHED | rushTicket | { ticket, items[], merchantId } |
TICKET_STATUS_CHANGED | markTicketReady, markTicketServed | { ticket, items[], merchantId } |
ITEM_VOIDED | voidTicketItem | { ticket, item, merchantId } |
ITEM_STATUS_CHANGED | startCookingItem, markItemReady, markItemServed | { ticket, item, merchantId } |
6. Services
6.1. KitchenTicketService
Source: src/services/kitchen-ticket.service.tsDI Dependencies: SaleOrderRepository, SaleOrderItemRepository, KitchenStationRepository, KitchenTicketRepository, KitchenTicketItemRepository, SaleSocketEventService
| Method | Description |
|---|---|
sendToKitchen | Validates order + items + station, creates ticket + items in transaction. Supports idempotency key to prevent duplicate tickets. |
voidTicket | Voids all active items (PENDING/COOKING/READY) in single SQL update. SERVED items untouched. Auto-progresses ticket. Stores void reason in metadata. |
rushTicket | Sets priority=1 and stores rush reason. Non-transactional (simple update). |
markTicketReady | Bulk updates all PENDING/COOKING items to READY. Auto-progresses ticket. |
markTicketServed | Bulk updates all READY items to SERVED. Auto-progresses ticket. |
6.2. KitchenTicketItemService
Source: src/services/kitchen-ticket-item.service.tsDI Dependencies: KitchenTicketRepository, KitchenTicketItemRepository, SaleSocketEventService
| Method | Description |
|---|---|
voidTicketItem | Voids single item (guards canVoid). Auto-progresses parent ticket. |
startCookingItem | PENDING → COOKING. Sets startedAt. Auto-progresses ticket to PROCESSING. |
markItemReady | COOKING → READY. Sets readyAt. May auto-progress ticket to READY. |
markItemServed | READY → 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
| Repository | Entity | Custom Methods |
|---|---|---|
KitchenTicketRepository | KitchenTicket | getNextSequence, findByIdempotencyKey, evaluateTicketAutoProgression |
KitchenTicketItemRepository | KitchenTicketItem | findByTicketId, findBySaleOrderItemId, findActiveByTicketId |
KitchenStationRepository | KitchenStation | Standard 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):
- All items VOIDED → ticket
VOIDED - All SERVED/VOIDED (1+ SERVED) → ticket
COMPLETED - All READY/SERVED/VOIDED → ticket
READY - Any COOKING/READY/SERVED → ticket
PROCESSING - Otherwise → no change (remains PENDING)
Each step uses EXISTS/NOT EXISTS subqueries to avoid race conditions.
8. Permissions
| Code | Action | Description |
|---|---|---|
KitchenTicket.send | CREATE | Send items to kitchen |
KitchenTicket.void | DELETE | Void a kitchen ticket |
KitchenTicket.rush | UPDATE | Mark ticket as rush |
KitchenTicket.updateTicketStatus | UPDATE | Bulk status updates (KDS) |
KitchenTicketItem.voidItem | DELETE | Void single item |
KitchenTicketItem.updateItemStatus | UPDATE | Item 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 → COMPLETED9.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 COMPLETED9.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}/void10. Frontend Integration
10.1. POS Terminal (FOH)
| Responsibility | Implementation |
|---|---|
| Send unsent items to kitchen | POST /sale-orders/{id}/kitchen-tickets/send |
| Void items/tickets on guest cancellation | POST /kitchen-ticket-items/{id}/void or /kitchen-tickets/{id}/void |
| Qty/modifier/replacement changes | Void old ticket or item → re-send with correct data |
| Rush a ticket | POST /kitchen-tickets/{id}/rush |
| Track kitchen status per order | Subscribe to wr:observation/sale-orders/{saleOrderId}/kitchen |
| Idempotency on retry | Include idempotencyKey in send requests |
| Pre-void validation | Check item status before void — warn if COOKING/READY, block if SERVED |
10.2. KDS Screen (BOH)
| Responsibility | Implementation |
|---|---|
| List active tickets for station | GET /kitchen-tickets?filter[kitchenStationId]={id}&filter[status][$ne]=505_CANCELLED |
| Progress items through stages | POST /kitchen-ticket-items/{id}/start-cooking, /ready, /served |
| Bulk complete ticket | POST /kitchen-tickets/{id}/ready or /served |
| Display rush indicator | Check ticket.priority === 1 |
| Display void indicator | Flash/highlight voided items for chef acknowledgment |
| Sort by priority + time | Order by priority DESC, pendingAt ASC |
| Real-time updates | Subscribe to wr:observation/kitchen-stations/{stationId} |
10.3. Concurrency Handling
| Mechanism | Description |
|---|---|
| Idempotency key | Include idempotencyKey in send requests (e.g., {saleOrderId}-{timestamp}-{random}) — prevents duplicate tickets on retry |
| SaleOrderItem validation | sendToKitchen validates each saleOrderItemId exists — if another waiter deleted the item, request fails with 404 |
| Status guard on void | Voiding an already-VOIDED or SERVED item returns 400 — FE should handle gracefully and refresh state from WS |
| WS-driven state | On WS events, refresh ticket/item state rather than relying on local state |
11. Data Model Reference
KitchenTicket
| Field | Type | Description |
|---|---|---|
id | string | Snowflake ID |
ticketNumber | string | Unique identifier (timestamp-snowflake) |
saleOrderId | string | Parent sale order |
merchantId | string | Merchant owner |
kitchenStationId | string? | Assigned station (nullable) |
status | string | Current status (auto-progressed) |
priority | number | 0=normal, 1=rush |
sequence | number | Order within SaleOrder |
pendingAt | Date | When created |
processingAt | Date? | When first item started cooking |
readyAt | Date? | When all items ready |
completedAt | Date? | When all items served |
voidedAt | Date? | When voided |
metadata | object | { note?, idempotencyKey?, rushReason?, voidReason? } |
KitchenTicketItem
| Field | Type | Description |
|---|---|---|
id | string | Snowflake ID |
kitchenTicketId | string | Parent ticket |
saleOrderItemId | string | Linked sale order item |
quantity | string | Quantity (decimal as string) |
status | string | Current status |
startedAt | Date? | When cooking started |
readyAt | Date? | When marked ready |
servedAt | Date? | When served to guest |
voidedAt | Date? | When voided |
metadata | object | { productMetadata?, modifiers?, specialInstructions?, voidReason? } |
12. Related Documentation
| Document | Description |
|---|---|
| Sale Order | SaleOrder entity, item modes, status lifecycle |
| Real-Time Events | WebSocket infrastructure, Signal integration |
| Checkout Flow | Order checkout and payment flow |