14 · POS & Mobile POS
Complete inventory of checkout surfaces, register/shift, cart/tender/finalize,
and the critical distinction between Mobile POS the UI and
/api/mobile the alternate device API.
posApi → /api/pos/* with a platform session.
Do not build new checkout against /api/mobile or /api/kiosk
unless you intentionally want the device-credential unattended path.
How you open POS
flowchart TB
LOGIN[Platform login · session cookie] --> POS["/pos"]
POS --> ENTRY[PosEntryRoute]
ENTRY -->|viewport ≤768 or pref mobile| MOB[MobilePOS.jsx]
ENTRY -->|desktop / pref modern| MOD[ModernPOS.jsx]
BOOK["/pos/mobile"] --> MOB
KURL["/pos/kiosk"] --> KIOSK[Kiosk.jsx]
KDS["/pos/kitchen"] --> KITCHEN[KitchenDisplay · KDS]
BAR["/pos/bar"] --> STUB1[Placeholder]
REST["/pos/restaurant"] --> STUB2[Broken placeholder]
HAND["/pos/handheld"] --> STUB3[Broken placeholder]
| URL | Component | Status |
|---|---|---|
/pos | PosEntryRoute → Modern or Mobile | primary |
/pos/mobile | MobilePOS always | primary |
/pos/kiosk | Kiosk.jsx + KioskLayout | real UI |
/pos/kitchen | KitchenDisplay | KDS |
/pos/bar | BarDisplay | placeholder |
/pos/restaurant | — | throws / not built |
/pos/handheld | — | throws / not built |
Layout preference: localStorage posLayoutPreference =
modern | mobile | auto.
One-shot: /pos?layout=modern or ?layout=mobile.
See lib/posLayoutPreference.js.
All POS-related surfaces
| Surface | File | ~Size | API | Notes |
|---|---|---|---|---|
| Modern POS | pages/pos/ModernPOS.jsx |
~2.5k lines | /api/pos |
Full register: gift, loyalty, seating, fuel module, platform catalog |
| Mobile POS | pages/pos/MobilePOS.jsx |
~2.4k lines | /api/pos |
Touch layout; send-to-kitchen; no gift/loyalty/seating/fuel/catalog enrich UI |
| Standard POS | pages/pos/StandardPOS.jsx |
~1.7k lines | /api/pos |
Older parallel full UI; gift/loyalty; offline-heavy |
| Kiosk | pages/pos/Kiosk.jsx |
~640 lines | /api/pos via posApi |
Self-checkout style; still needs staff session today |
| KioskSelfServe | KioskSelfServe.jsx |
— | None | Demo only — hardcoded products |
| Kitchen (KDS) | KitchenDisplay.jsx |
~17k | /api/kds |
Polling ~3s; seat chips from tickets |
| Bar | BarDisplay.jsx |
~10 lines | — | Placeholder |
| Storefront | pages/store/Storefront.jsx |
— | /api/pos cart finalize |
Customer e-com reuses POS spine |
| Open registers | admin/operations/OpenRegisters.jsx |
— | register open-shifts / admin-close | /admin/open-registers |
| Shift history | RegisterShiftHistory.jsx |
— | shift-history | /admin/register-shift-history |
Modern vs Mobile — feature parity
Both share register ceremony and the same cart/tender/finalize API. Differences are UI capability.
| Capability | Modern | Mobile |
|---|---|---|
| Platform session required | yes | yes |
| Register PIN + open/close shift | yes | yes |
canTransactOnRegister gate | yes | yes |
| Device selector / POS settings | yes | yes |
| Catalog search + barcode | yes | yes |
| Platform UPC catalog enrich | yes | no |
| Customer attach / search | yes | yes |
| Cash / card / terminal / crypto | yes | yes |
| Gift card tender UI | yes | no |
| Loyalty tender UI | yes | no |
| Seating / floor picker | yes | no |
| Fuel / posModules | yes | no |
| Manual send-to-kitchen | limited | explicit button |
| Offline queue via posApi | yes | yes |
| Switch to other layout | — | → desktop Modern |
Canonical sale flow (Modern & Mobile)
flowchart TD A[Platform login] --> B[Device context
deviceKey · location · catalogs] B --> C[Register authenticate
email + POS PIN] C --> D{Register open?} D -->|no| E[Open shift + float] D -->|yes + staff match| F[canTransactOnRegister] E --> F F --> G[POST /api/pos/carts] G --> H[Add items · barcode · customer] H --> I[Optional metadata seat/table · Modern] I --> J[Tender cash/card/crypto/…] J --> K{Covered?} K -->|no| J K -->|yes| L[POST .../finalize] L --> M[CoreInvoicingService] M --> N[(Invoices + payments)] L --> O[Inventory / recipe consumption] L --> P[kitchen_tickets if preparable] H --> Q[Mobile: send-to-kitchen] Q --> P
PIN session + active cart id: lib/posLocalPersistence.js
(keys scoped by tenant + device). Deep step list: OAO Appendix B.
Backend API surface
A · Canonical — /api/pos
pos.routes.js (~4.2k lines) + pos-register.routes.js.
Middleware: session + tenant + optional device + idempotency + rate limits.
| Area | Paths |
|---|---|
| Cart |
GET/POST /carts ·
GET /carts/:id ·
PATCH .../metadata ·
POST .../customer ·
POST .../items ·
DELETE .../items/:lineId ·
POST .../items/:lineId/qty ·
POST .../recalc ·
POST .../tip ·
POST .../send-to-kitchen ·
GET .../invoice ·
POST .../finalize
|
| Tenders |
cash · card · giftcard · loyalty · crypto ·
coinbase · coingate · stripe-terminal · square-terminal ·
ingenico · verifone · adyen · paypal
Many backends; only a subset have first-class UI |
| Register |
POST /register/authenticate ·
open · status · close ·
open-shifts · shift-history ·
admin-close · admin-force-signout
|
| Device / catalog |
GET /catalog ·
GET /settings (+ posModules) ·
assign-catalog · assign-location · available-catalogs · available-locations
|
B · Client wrapper — frontend/src/pos/api.js
All mutating cart/tender/finalize calls go through tryOnlineThenQueue
(offline mutation queue). Service worker for full offline shell is still disabled.
C · Parallel device APIs (not used by main UIs)
| Mount | Auth | Used by MobilePOS / Kiosk.jsx? |
|---|---|---|
/api/mobile/* |
deviceAuth (+ optional session) | No — thin cart/sync/finalize via CartService |
/api/kiosk/* |
deviceAuth | No — Kiosk.jsx uses posApi + session |
Naming traps
flowchart LR
subgraph UI["What staff open"]
MPOS[Mobile POS page
MobilePOS.jsx]
KUI[Kiosk.jsx]
end
subgraph API["What they call"]
POSAPI["/api/pos/* · session"]
end
subgraph ALT["Separate unattended routers"]
MAPI["/api/mobile/*"]
KAPI["/api/kiosk/*"]
end
MPOS --> POSAPI
KUI --> POSAPI
MAPI -.->|not used by MPOS| X[ ]
KAPI -.->|not used by Kiosk.jsx| X
/pos / /pos/mobile.
It does not mean the Express tree under backend/routes/api/mobile/.
Stubs, orphans, and product gaps
| Item | Type | Action hint |
|---|---|---|
| Bar display | Placeholder UI | Build or hide route |
| Restaurant / handheld routes | Throwing placeholders | Build or remove from App.jsx |
| KioskSelfServe | Hardcoded demo catalog | Wire to posApi or archive |
| SplitTenderDialog | Component exists, not imported in Modern/Mobile | Wire multi-tender UX or delete |
| POSModeRouter | Legacy ?mode= switcher | Prefer PosEntryRoute + explicit paths |
| Mobile gift/loyalty/seating/catalog/fuel | Parity gaps | Product backlog |
| Register ceremony for solo ops | ICP friction | Optional simple mode (audit S6-3) |
| Offline service worker | Disabled | Queue works; full offline shell does not |
| Extra tender backends | API without UI | Document as integrate/defer |
File anchors
| Concern | Path |
|---|---|
| Entry routing | frontend/src/components/pos/PosEntryRoute.jsx |
| Modern / Mobile UIs | frontend/src/pages/pos/ModernPOS.jsx · MobilePOS.jsx |
| posApi client | frontend/src/pos/api.js |
| PIN + cart persistence | frontend/src/lib/posLocalPersistence.js |
| Layout preference | frontend/src/lib/posLayoutPreference.js |
| Register UI | components/POS/RegisterLoginScreen.jsx · RegisterOpeningDialog.jsx |
| Seating / fuel | PosSeatingPicker.jsx · pos/modules/* |
| Cart + finalize backend | backend/routes/pos.routes.js |
| Register backend | backend/routes/pos-register.routes.js |
| Tenant POS SQL helpers | backend/utils/posTenantQueries.js |
| Invoicing | backend/services/coreInvoicing.service.js |
| Device API alts | backend/routes/api/mobile/index.js · api/kiosk/index.js |
| OAO chapters | 06-POS-SYSTEMS.md · Appendix B · KDS 08 |