Skip to content

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 Bar or Kitchen item 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 order alert, 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 order alert, 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 ordinary New order alert 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× Sandwich

A 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× Americano

A 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.000

Once 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." A Cash order or Verify COD order alert leads with the amount to collect (or check) instead, since nothing has to be made yet.

Setting up a phone ​

  1. Install the KDS app on the dedicated phone and log in with a staff account — the same username and password used on the POS.
  2. 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.
  3. 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.
  4. 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 None default 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/None tag (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 order one — nothing is made until a barista confirms the guest from their checkout photo; the ordinary New order alert 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/kds import graph alongside /pos and /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 in libs/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), tables kds_devices and kds_notifications (migrations 000031/000032)
  • The cash-pending alert reuses this outbox rather than a second one: kds_notifications.kind (order_paid | cash_pending, migration 000035) 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, and cash_pending skips both ShouldNotify'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, calling POST https://exp.host/--/api/v2/push/send, configured by EXPO_PUSH_ACCESS_TOKEN, KDS_PUSH_SOUND, and KDS_DISPATCH_INTERVAL_SECONDS in apps/api/.env
  • The single trigger point: payTransaction (apps/api/domain/transaction_usecase.go), called by both the POS PUT /transactions/{id}/pay route 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 reuses categories.station instead 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 the kind column 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_paid kind rather than adding a cod_approved one: a new kind for approval would make payTransaction also send order_paid, buzzing the bar twice for one order; a fourth kds_notifications.kind value, cod_verification, is enqueued the instant a COD checkout completes (bypassing ShouldNotify's station gate and the staleness check, same as cash_pending), and BuildKdsPushMessage (apps/api/domain/kds_notification_entity.go) adds the COD — collect Rp X at pickup line to order_paid for 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 reuses order_paid instead of a new kind

Built with VitePress. Content lives in docs-site/ — see the README for how to run it locally.