Kitchen Display System (KDS)
What it does
The KDS app is a second, dedicated phone that sits on the bar or in the kitchen and buzzes the moment a transaction is paid — at the counter or from a guest's table order. It has exactly two screens: log in with the same staff credentials used everywhere else, then set up the device with a name ("Andi's phone", "Counter tablet"). Once registered, the app's whole job is to sit there — open or closed, screen on or off — and let a push notification land with sound and vibration whenever an order comes in. There's no order queue or ticket list in this app; that's read off the Transactions list or the printed order slip, the same way it is today.
Why it matters
Before this, nothing told the bar or the kitchen that an order had arrived. A guest paying from their table via QRIS gets no counter interaction and no verbal handoff at all — the paid transaction just sits in the database until someone happens to look at the POS or remembers to print the slip. The KDS app closes that gap the way every delivery-app merchant tablet already does: push notification, loud, on a phone that stays on the counter — not "the cashier will notice."
How a payment becomes an alert
Every transaction — POS or table order — routes through the same station tagging that already splits the printed order slip into Bar and Kitchen sections:
- A transaction with at least one
BarorKitchenitem notifies. The moment it's marked paid, every registered KDS phone buzzes once — this venue's two staff usually split the bar and the kitchen, but sometimes both cover the bar, so one alert to every phone means nobody is ever silently left out of a station nobody claimed. - A transaction whose items are all in a
None-station category — a board-game ticket, a deposit — notifies nobody. Nothing has to be made, so nothing summons anyone. This is exactly the same rule that already decides whether a category shows up on the printed slip; get the station right once on the Categories screen and both surfaces agree. - A sale settled the next business day doesn't notify. A cashier closing out yesterday's tab this morning shouldn't buzz the bar for a "new" order that isn't one.
- A guest checking out with cash at the cashier buzzes every phone a second time, before any money moves. The station rule doesn't gate this one — an order of nothing but a board-game ticket still needs someone at the till to collect it — so it fires regardless of what's in the cart. The same transaction buzzes again, as a normal
New orderalert, once the cashier actually takes the payment. - A guest checking out with COD buzzes every phone with a distinct "verify this" alert the instant the order is placed — not once it's paid, and not the normal
New orderalert, since nothing should be made until a barista has confirmed the guest from the photo (see Verifying a COD Order). The station rule and the business-day staleness check are both bypassed, the same as the cash alert above, because this is a request for a human decision, not a preparation signal. The ordinaryNew orderalert follows once the barista approves it — with an extra line naming what to collect, since the money still hasn't moved.
Reading the alert
One notification, shared identically by every phone, with the items grouped under the station that's actually theirs:
New order #12 — Table 4
BAR: 2× Kopi Susu Gula Aren, 1× Americano · KITCHEN: 1× SandwichA guest paying cash at the table order app produces a second, distinctly titled alert first, before the transaction is paid:
Cash order #12 — Table 4
Collect Rp 45.000 at the counter · BAR: 2× Kopi Susu Gula Aren, 1× AmericanoA guest checking out with COD produces a different alert still, aimed at the barista rather than the bar — there's nothing to make yet, only a photo to look at:
Verify COD order #12 — Meja 4
Check the photo in the POS · Rp 45.000Once the barista approves it, the normal New order alert follows, with one extra line for a still-unpaid COD order:
New order #12 — Meja 4
BAR: 2× Kopi Susu Gula Aren, 1× Americano
COD — collect Rp 45.000 at pickup- The number is the same daily transaction number already printed on the slip and shown on the guest's own order-status page — whatever the staff member calls out, it's what everyone else is already looking at.
- The line after it is grouped Bar, then Kitchen — a coffee-only order still says
BAR: …so reading "not mine" takes the same half-second as reading "mine." ACash orderorVerify COD orderalert leads with the amount to collect (or check) instead, since nothing has to be made yet.
Setting up a phone
- Install the KDS app on the dedicated phone and log in with a staff account — the same username and password used on the POS.
- Give the device a name (e.g. "Bar phone") and grant notification permission when asked. If permission was denied earlier, the app links straight to the phone's notification settings instead of asking a second time, since a second in-app prompt after a denial does nothing on either platform.
- Tap Register this device, then Send test notification to confirm sound and vibration work before relying on it during service — waiting for a real order to find out is the wrong time to discover the phone was muted.
- Leave the phone plugged in, with Do Not Disturb off and battery optimization disabled for the app, on the counter for the rest of the shift.
If a phone stops receiving orders
- Check the test notification first. Send test notification on the device-setup screen is the fastest way to tell "this phone stopped receiving" from "nobody's paid an order that qualifies yet."
- Confirm the category is tagged correctly. A drinks category accidentally left at the
Nonedefault notifies nobody; check its station on the Categories list, which shows every category's station next to its name. - Re-register after a reinstall. Reinstalling the app or updating the OS can rotate the device's push token; registering again from the setup screen replaces the old token under the same device rather than creating a second, dead entry.
- Orders arriving in a burst the moment the screen is unlocked means the phone's battery optimizer is holding them. Alerts are sent at high delivery priority precisely so a locked, screen-off phone still wakes for them, but Android can still override that for an app it has put under battery restrictions — set the KDS app to Unrestricted battery usage in the phone's app settings.
- Log out or unregister a phone that's leaving service — both stop it from receiving further orders, which matters for a lost or replaced phone.
- A push service outage is the fallback case this app was built to survive. If Expo (or the phone's push service) is briefly down, the order is still exactly where it always was: on the Transactions list and the printed order slip.
Key capabilities
- Two screens, one job — login and device setup; no queue, no ticket list, no bump button in this app yet.
- One alert per paid transaction, to every phone — no per-device station subscription to keep correct as staff move between the bar and the kitchen mid-shift.
- Same station rule as the printed slip — a category's
Bar/Kitchen/Nonetag (Categories) decides both whether the slip prints a section and whether the KDS buzzes, so the two surfaces can never disagree. - Loud by design — an Android notification channel created at maximum importance with sound and vibration, built to be heard over a busy room.
- Arrives on a locked phone — alerts are sent at high delivery priority, so the phone wakes for an order instead of holding it until someone turns the screen back on.
- Send test notification — confirms a device is set up correctly without waiting for a real order.
- Unregister and log out — either stops a phone from receiving further orders.
- A cash order-app checkout buzzes twice — once the instant it's created, so a barista can be at an unstaffed till before the guest walks down, and once more when the cashier actually takes the payment — see Order Checkout (QRIS, Cash & COD).
- A COD checkout buzzes with a distinct "verify this" alert, not the normal
New orderone — nothing is made until a barista confirms the guest from their checkout photo; the ordinaryNew orderalert follows once they approve it, carrying an extra line for the still-unpaid amount.
For engineers
- App:
apps/kds-mobile— Expo, two screens, its own@gatherloop-pos/ui/kdsimport graph alongside/posand/order(docs/trd-ui-presentation-split-by-app.md) - Screens and handler:
libs/ui/src/presentation/views/screens/kds/{KdsLoginScreen,KdsDeviceSetupScreen}.tsx,libs/ui/src/presentation/handlers/kds/KdsDeviceSetupHandler.tsx - Device-side push capability, behind an interface so tests never touch
expo-notifications:libs/ui/src/domain/repositories/pushToken.ts, implemented inlibs/ui/src/data/native/ExpoPushTokenRepository.ts - Backend station rule and outbox:
apps/api/domain/kds_notification_routing.go(ShouldNotify,StationLines),apps/api/domain/kds_notification_usecase.go(EnqueueForPaidTransaction,DispatchPending), tableskds_devicesandkds_notifications(migrations000031/000032) - The cash-pending alert reuses this outbox rather than a second one:
kds_notifications.kind(order_paid|cash_pending, migration000035) with a(transaction_id, kind)unique key in place of the original single-column one, so the same transaction can hold one row of each kind;BuildKdsPushMessage(apps/api/domain/kds_notification_entity.go) switches title and body on it, andcash_pendingskips bothShouldNotify's station gate and the business-day staleness check, since it's enqueued the instant the transaction exists - Delivery gateway:
apps/api/data/expopush/kds_push_repo.go, callingPOST https://exp.host/--/api/v2/push/send, configured byEXPO_PUSH_ACCESS_TOKEN,KDS_PUSH_SOUND, andKDS_DISPATCH_INTERVAL_SECONDSinapps/api/.env - The single trigger point:
payTransaction(apps/api/domain/transaction_usecase.go), called by both the POSPUT /transactions/{id}/payroute and the order app's QRIS confirmation — one insertion point for every way a transaction becomes paid - Design doc:
docs/prd-kds-order-notifications.md— why payment (not creation) is the trigger (D1), why routing reusescategories.stationinstead of a new taxonomy (D2), why one notification goes to every device instead of per-device station subscriptions (D24), and what's explicitly deferred to a future KDS project — the order queue, ticket ages, and the bump button (D16) - Follow-up design doc:
docs/prd-order-cash-payment.md— the one exception to "payment is the trigger": D9 on why an unpaid cash order still notifies, and D8 on thekindcolumn that made a second notification per transaction expressible instead of forbidden by the outbox's own unique key - COD reuses the same outbox and the same
order_paidkind rather than adding acod_approvedone: a new kind for approval would makepayTransactionalso sendorder_paid, buzzing the bar twice for one order; a fourthkds_notifications.kindvalue,cod_verification, is enqueued the instant a COD checkout completes (bypassingShouldNotify's station gate and the staleness check, same ascash_pending), andBuildKdsPushMessage(apps/api/domain/kds_notification_entity.go) adds theCOD — collect Rp X at pickupline toorder_paidfor a transaction that is COD and still unpaid - Follow-up design doc:
docs/prd-order-cod-payment.md— FR-9 on what the KDS hears, and D6 on why approval reusesorder_paidinstead of a new kind