#R08 — Client Applications & Device Fleet Management
Scope: every screen the product ships, the framework decision for each, app distribution, MDM/fleet ops, and the POS-specific UX constraints that engineers routinely underestimate.
Date: 2026-08-18. Labels: [verified] = checked this session with a URL, [known] = confident from training data, [estimate] = reasoned guess.
Reads alongside: r01-hardware-peripherals.md (printers, terminals, GMS/EDLA trap), r04-offline-sync-domain.md (sync core, hub election), r03-fiscal-regulatory.md, r06-crm-loyalty-privacy.md. Effort here excludes the sync core, the printing stack, fiscal drivers, payments certification, and backend — those are costed in their own docs.
#0. Bottom line up front
- You will ship 11–14 distinct client surfaces. That is the real scope of "a POS + CRM", and it is why this category eats EUR 5M. Ranked honestly, only three are required for a first paying customer: the POS terminal app, the back-office web console, and a kitchen output path (printer is enough; KDS is a fast-follow).
- Framework verdict: Kotlin + Jetpack Compose, Android-first, structured as Compose Multiplatform modules from day one. Web (React/TypeScript) for back office, guest ordering, and QR. No Flutter, no React Native, no dual-native, no Electron. The decisive argument is not rendering performance — it is that every peripheral and payment SDK in the certified matrix is a Java/Kotlin AAR, and bridging 10–14 of them into a JS or Dart runtime costs 4–7 engineer-months of pure glue plus permanent maintenance. Kotlin makes that cost zero. Section 2.3 has the arithmetic.
- The switch condition is explicit: if the beachhead flips to Western EU or the US, iPad share rises materially and Compose-Multiplatform-on-iOS becomes a load-bearing bet rather than a free option. In that world React Native (New Architecture) is the safer pick. Decide the beachhead before you write the first Gradle file.
- Two operational facts will hurt more than any framework choice:
- Android cannot downgrade an app in place. Uninstall/reinstall wipes app-private storage — i.e. the offline order journal. Therefore rollback does not exist; you have forward-fix + staged rollout + server-side kill switches, or you have Friday-night outages.
- Silent app install on Android requires Device Owner mode, which can only be established at factory-reset time. A device an installer unboxed and set up "normally" can never be silently updated again without wiping it. This makes the 20-minute install runbook a product requirement, not ops hygiene.
- Apple's App Review is not the P1 risk people think — 90% of submissions clear in under 24h and expedited review is typically ~24h
[verified]. The real Apple risks are: no rollback, Guideline 4.2.6 (no per-restaurant white-label apps for SMB), the multicast entitlement, and the Local Network permission prompt that silently kills printer discovery on install day. - Budget EUR 12–30/site/month for MDM at 6 devices/site. Against a EUR 79–129/site/month subscription that is 10–25% of gross margin. Recommendation: cheap MDM for enrolment + kiosk lock + OS update windows only (Mosyle EUR ~1/device, Hexnode EUR ~1–1.8) and put diagnostics/telemetry/remote-config inside your own app, where it works offline and you control it. Esper (USD 2–6/device, 25-device min
[verified]) is worth its premium only once you have >200 sites or a self-order kiosk fleet.
#1. Surface inventory and MVP ranking
#1.1 The full list
| # | Surface | Primary user | Device | Why it exists (the job it does) | Cost of omitting it |
|---|---|---|---|---|---|
| 1 | POS terminal app | Server, cashier, manager | Sunmi D3 Pro 15.6" / Elo I-Series / iPad 11" | Take the order, take the money, close the shift. This is the product. | No product |
| 2 | Back-office web console | Owner, manager, our support | Any browser (desktop-first, 1280px+) | Menu, prices, staff, roles, reports, device config, CRM. Where the owner lives. | No product |
| 3 | Kitchen printer path | Kitchen | Epson TM-m30III / TM-U220II | Get the ticket to the line. | No product (costed in r01) |
| 4 | KDS (kitchen display) | Line cooks | 15–21.5" Android AIO or Elo, wall/shelf mount | Replaces paper: ticket state, timers, all-day counts, bump. Sells against paper on ticket-loss and speed-of-service. | Lose every site with >2 prep stations; lose the "modern" story |
| 5 | Handheld waiter app | Server | Sunmi V3 Mix / L2s Pro / phone (5–6") | Order at the table, fire courses, take payment tableside. Cuts table-turn time ~10–15% [estimate] in table service. | Lose most full-service restaurants in DE/AT/CZ where tableside is the norm |
| 6 | Expo / pass screen | Expeditor | 21.5"+ landscape | Assembles the whole order across stations; the counterpart to the per-station KDS. | A KDS mode, not a separate app. Omitting it breaks any kitchen with 3+ stations |
| 7 | Customer-facing display (CFD) | Guest at the counter | Second screen on the AIO (Sunmi D3 Pro dual, Elo) or a 10" tablet | Shows the running bill, the loyalty prompt, the tip screen, the QR to join loyalty. This is the single highest-yield CRM capture point (see r06 §2). | Loyalty capture rate drops hard; some markets require price transparency at the counter |
| 8 | Guest web ordering + loyalty wallet | Guest, off-premise | Mobile browser, PWA, Apple/Google Wallet pass | Order-ahead / pickup / delivery-direct. The revenue argument for the whole CRM: commission-free orders vs 25–30% on Glovo/Wolt/Bolt Food. | Lose the CRM's headline ROI story |
| 9 | QR table-ordering PWA | Guest, on-premise | Guest's phone, no install | Order + pay from the table. Genuinely popular in PL/CZ/UA cafés and bars; near-zero adoption in DE/AT fine dining. | Lose a differentiator in the beachhead; low cost because it reuses #8 |
| 10 | Owner mobile app | Owner, multi-site operator | iOS + Android phone | Today's sales, live tickets, cash-drawer alerts, remote void approval, staff clock-in anomalies. Drives daily engagement → drives retention. | Retention/NPS hit; no daily habit; measurable churn driver [estimate] |
| 11 | Self-order kiosk | Guest | 21–27" portrait + unattended EMV terminal | QSR/fast-casual labour substitution. Average check typically +8–15% vs counter [estimate]. | Locked out of QSR/fast-casual entirely |
| 12 | Drive-thru | Guest in car | Order confirmation board, HME/3M headsets, lane sensors, outdoor terminal | US/PL QSR chains only. | Nothing, in SMB. Do not build. |
| 13 | Fleet/device agent + admin console | Us (support) | Embedded in each app + internal web | Provision, update, diagnose, telemetry, remote-config. | Support cost per site goes up 3–5× [estimate]; you cannot scale past ~50 sites |
| 14 | Internal support console | Our support/CS | Web | Impersonate a site read-only, inspect device state, pull logs, replay an order, reissue a receipt. | Every ticket becomes an engineer's problem |
Two surfaces are missing from the brief's list and belong in it: #13 fleet agent and #14 internal support console. Teams skip them and then discover that customer #30 costs the same engineering attention as customer #1.
#1.2 MVP ranking (build order)
| Rank | Surface | Tier | Rationale for the position |
|---|---|---|---|
| 1 | POS terminal app | T0 | Nothing sells without it |
| 2 | Back-office web console (menu + staff + basic reports) | T0 | The owner's buying evaluation happens here, not at the till |
| 3 | Kitchen printing | T0 | Costed in r01 |
| 4 | Fleet agent (config push, OTA, log pull, telemetry) | T0 | Cheap now, impossible to retrofit onto 200 deployed devices |
| 5 | KDS + expo mode | T1 | First upsell; first "modern POS" proof point |
| 6 | Handheld waiter app | T1 | Required for full-service. ~70% code reuse from #1 |
| 7 | Owner mobile app (thin, read-only + approvals) | T1 | 1.5 eng-months for a disproportionate retention effect |
| 8 | Internal support console | T1 | Needed by roughly customer #20 |
| 9 | QR table-ordering PWA | T1/T2 | Cheap once #10 exists; strong beachhead differentiator |
| 10 | Guest web ordering + wallet pass | T2 | The CRM ROI story, but a whole payments+menu+delivery surface of its own |
| 11 | CFD | T2 | 1.2 eng-months; move to T1 if loyalty capture is the wedge |
| 12 | Self-order kiosk | T3 | +4.5 eng-months app plus 5–8 for unattended EMV certification (r01 item D) |
| 13 | Drive-thru | Never (SMB) | Enterprise QSR feature, enterprise sales motion |
Explicit non-goals for v1: drive-thru; native per-restaurant white-label guest apps (see §4.6); a Windows/desktop POS client; a smartwatch app; anything on a TV OS.
#2. Framework decision — hard verdict
#2.1 The five tests, stated as falsifiable requirements
| Test | Concrete pass criterion |
|---|---|
| (a) Offline + SQLite | Append 1 order event to a local WAL-mode SQLite DB and return control in ≤ 5 ms p99 on a Sunmi D3 (RK3568, 2 GB RAM). Survive process kill mid-order with zero lost keystrokes. Full-text search 3,000 menu items in ≤ 30 ms. |
| (b) Raw hardware access | Open a TCP socket to printer:9100; bind an AIDL service (IWoyouService) for the built-in printer/drawer; receive a scanner broadcast intent; call a payment terminal SDK (Adyen/SumUp/Viva/PAX) and handle its async callbacks; read RS-232 from a scale via a USB-serial adapter. |
| (c) 60 fps under a rush | p99 frame time ≤ 16.7 ms with a 200-item menu grid on screen, an open ticket of 40 lines, an active MQTT connection, and a print job in flight. Cold start to interactive ≤ 1.5 s on the D3. |
| (d) One codebase across iPad + Android POS + KDS | ≥ 80% shared code across the counter surfaces; a new modifier rule ships once, not three times. |
| (e) Hiring | You can hire a mid-level engineer in Kyiv/Lviv/Warsaw/Kraków in ≤ 6 weeks at ≤ EUR 4,500/mo gross-of-taxes contractor rate [estimate] and have them productive in ≤ 3 weeks. |
#2.2 Candidate matrix
| (a) Offline+SQLite | (b) Raw hardware | (c) 60 fps | (d) One codebase | (e) Hiring | Verdict | |
|---|---|---|---|---|---|---|
| Kotlin + Compose (Android-only) | Best-in-class: Room/SQLDelight, direct JNI | Native. Zero bridge. Every vendor SDK is a Java AAR | Excellent with Baseline Profiles + R8 full mode | Android POS + KDS + CFD + handheld + kiosk = one codebase. iPad excluded | Huge Android pool in UA/PL/RO | CHOSEN |
| Compose Multiplatform (adds iOS) | Same, via SQLDelight | Android native; iOS needs Swift interop (but iOS has far fewer SDKs to bind) | Stable for iOS since May 2025; UI stable on Android/iOS/desktop, Web/Wasm still beta as of May 2026 [verified] | Adds iPad at ~15–25% extra [estimate] | KMP-specific experience thin, but you hire Android devs not "KMP devs" | CHOSEN as the module structure, iOS target activated later |
| React Native (New Architecture) | op-sqlite / expo-sqlite are fine; JSI makes sync calls cheap. Bridgeless default since 0.78 [verified] | Every vendor SDK needs a TurboModule. 0.2–0.6 eng-mo each × 10–14 SDKs | Achievable but requires discipline (FlashList, memo hygiene); JS thread is a shared resource with your sync engine | Best breadth: iPad + Android + (with RN-Web, partially) browser | Largest, cheapest pool in CEE by a wide margin | Runner-up. Becomes first choice if iPad is primary |
| Flutter | Excellent (drift/sqflite); isolates for the sync engine | Same platform-channel tax as RN, in Dart | Best raw rendering. Risk: Impeller is the only renderer on modern Android as of 2026 [verified] — exotic Rockchip/Mediatek AOSP GPU drivers on cheap POS AIOs are an unvalidated surface [estimate — must test on the actual D3/Elo units] | Good | Dart pool in CEE is real but ~4–6× smaller than TS [estimate] | Rejected: same bridging tax as RN, smaller hiring pool, driver risk on the exact hardware we resell |
| Native Swift + native Kotlin | Best | Best | Best | Worst: 1.8–2.2× surface work [estimate] | Fine | Rejected. Fatal for S1 |
| Web / PWA in a WebView | OPFS+SQLite WASM works; quota and eviction are hostile | Disqualifying. No TCP sockets, no Bonjour, no vendor SDKs, no Web Bluetooth on iOS (r01 §2.5). WebView version is frozen on non-GMS Sunmi ROMs (r01 §1.3) | Adequate; janky on 2 GB devices | Great for the other surfaces | Largest pool | Rejected for the counter. Correct for #2, #8, #9, #14. |
| Electron / Tauri | Fine | Full native access on desktop | Fine | Irrelevant to tablets | Fine | Rejected as a POS client. Accept a small Tauri "site bridge" if you ever need to drive Polish fiscal printer DLLs or legacy serial gear from a Windows box (r01 §6.3) |
#2.3 The decisive argument: the SDK bridging tax
Enumerate the native SDKs the counter app must call in the first 24 months:
| SDK / interface | Language shipped | Bridge cost in RN/Flutter [estimate] |
|---|---|---|
Sunmi printer + drawer (IWoyouService AIDL) | Java AAR | 0.3 mo |
| Sunmi scanner broadcast + HID | Java | 0.15 mo |
Sunmi dual-screen / Presentation CFD | Java | 0.3 mo |
| Sunmi Pay Kernel (V2/P2 payment devices) | Java AIDL | 0.6 mo |
| Epson ePOS Android SDK | Java AAR | 0.4 mo |
| Star Android SDK / CloudPRNT | Java AAR | 0.4 mo |
| Zebra Link-OS / Brother QL SDK (labels) | Java AAR | 0.3 mo |
| Payment terminal SDK #1 (SumUp / Viva / Adyen Android) | Java/Kotlin AAR | 0.6 mo |
| Payment terminal SDK #2 (second acquirer, per market) | Java AAR | 0.5 mo |
PAX Neptune Lite (com.pax.dal.IDAL) if you go PAX | Java | 0.6 mo |
| Fiscal device bridge (PL Posnet/Elzab, RO AMEF) | Java/JNI wrapper | 0.5 mo |
Android Enterprise DevicePolicyManager kiosk/OTA controls | Java | 0.4 mo |
USB-serial for scales (UsbManager + FTDI/CP210x) | Java | 0.3 mo |
| Android Keystore / device attestation | Java | 0.2 mo |
| Total bridge glue | ≈ 5.5 eng-months |
Plus a permanent tax: every vendor SDK bump re-tests the bridge, every RN/Flutter major bump re-tests all of them. Budget 0.15–0.25 eng-months/quarter forever [estimate].
In Kotlin this number is zero. Five and a half engineer-months is ~14% of the entire client-app budget in §Build effort, and it buys nothing a customer can see. That is the verdict.
The honest counter-argument: RN would let you share TypeScript domain logic with the back office and guest PWA. But per r04 the authoritative domain is an event-sourced core that should live in one place and be deterministic — and KMP compiles that same Kotlin core to a JVM backend and to JS/Wasm for the web console if you want it. Kotlin gives you the shared-core benefit too, in the direction that matters.
#2.4 Verdict, stated as a commitment
Counter surfaces (POS, handheld, KDS, expo, CFD, kiosk):
Kotlin 2.x + Jetpack Compose, min SDK 26 (Android 8.0), target latest
Module layout from day 1:
:domain (KMP, pure Kotlin, no Android deps) <- shared with backend
:data (KMP, SQLDelight, sync client)
:ui-core (Compose Multiplatform: design system, POS widgets)
:ui-pos / :ui-kds / :ui-cfd / :ui-kiosk (CMP where possible)
:android-app (Android-only: AIDL, USB, DPM, vendor SDKs)
:ios-app (NOT built in v1; the seam exists)
Back office, guest ordering, QR PWA, internal support console:
TypeScript + React (Vite), no SSR-heavy framework for the console;
guest ordering gets SSR (Next.js or Remix) for SEO + first-paint
Optional site bridge (Windows/Linux, only if fiscal or legacy serial forces it):
Tauri 2.x + Rust, ~30 MB, no Chromium bundle
Languages the company maintains: Kotlin + TypeScript (+ Rust only if the bridge ships).What would flip this to React Native: (1) beachhead becomes US or Western EU with >40% iPad attach; (2) you decide to sell a per-restaurant branded native guest app (you shouldn't — §4.6); (3) you cannot hire 2 senior Android engineers in 8 weeks.
What would flip this to Flutter: essentially nothing, unless your existing team is already a Flutter team — in which case use Flutter and eat the 5.5-month bridging tax, because team familiarity is worth more than 5.5 months.
#2.5 What iPadOS forbids (the concrete list)
| Constraint | Detail | Consequence for a POS |
|---|---|---|
| No general USB host API | No third-party USB access on iPadOS. USB-C handles displays, UVC cameras, audio, Ethernet at OS level only. FTDI/CP210x serial adapters are invisible to apps. [known] | No USB printers, no USB scales, no USB cash-drawer interfaces. LAN or MFi Bluetooth only |
| Bluetooth Classic requires MFi | SPP accessories need MFi certification (paid by the accessory maker). BLE/GATT via CoreBluetooth is open. [known] | Epson TM-m30III BT works (MFi); a EUR 40 generic BT printer never will |
| Background execution | beginBackgroundTask gives ~30 s. Available background modes: audio, location, BLE central/peripheral, external-accessory, VoIP (deprecated pattern), background fetch (opportunistic), push. No long-lived listening socket. [known] | An iPad cannot be the LAN sync hub (r04 §2.3/2.7). A KDS iPad must be pinned foreground via Single App Mode or it stops receiving tickets |
| Bonjour/mDNS | Requires com.apple.developer.networking.multicast, granted by Apple on application only. [known] | Weeks of lead time, possible refusal. Manual-IP fallback is mandatory (r01 §2.6) |
| Local Network permission | iOS 14+ prompts once. If the installer taps "Don't Allow", every printer disappears and there is no programmatic re-prompt — only Settings. [known] | Top-3 install-day failure. The install wizard must test local network access and hard-block until granted |
| Kiosk mode | Autonomous Single App Mode (UIAccessibility.requestGuidedAccessSession) only works on a supervised device with your bundle ID in the MDM's ASAM allow list. Manual Guided Access is fragile (staff exit it). [known] | You cannot ship a locked-down iPad POS without ABM + MDM. Adds EUR 1–5/device/mo and a purchasing constraint (§5.2) |
| In-app purchase rules | Selling your SaaS subscription inside the merchant app invites 15–30% commission. [known — post-DMA/Epic rules are volatile] | Ship the app with sign-in only; all billing on the web |
| Guideline 4.2.6 | Apps generated from a template/generation service must be submitted from the client's own developer account. [known] | No white-label per-restaurant guest apps for SMB (§4.6) |
| DMA alternative distribution | iOS 17.4+/iPadOS 18+ permit alternative marketplaces in the EU, with notarization and a Core Technology Fee. Terms revised through 2025. [known — volatile, verify before relying on it] | A theoretical escape from App Review; not a plan |
#2.6 What Android POS vendors require
| Vendor / topic | Requirement | Impact |
|---|---|---|
| Sunmi printer/drawer | Bind the AIDL service (woyou.aidlservice.jiuiv5 / IWoyouService), or the newer printer library. Drawer via openDrawer(). [known — verify current package names at docs.sunmi.com] | Direct Kotlin call. Service can die; you need rebind-with-backoff |
| Sunmi scanner | Broadcast intent (...ACTION_DATA_CODE_RECEIVED) or HID keyboard emulation | Two code paths; HID mode steals focus from text fields |
| Sunmi dual screen (D3 Pro) | DisplayManager + Presentation, plus Sunmi's ext-display service for the small LCD variants | CFD is a second Activity in the same process; test what happens on process death |
| Sunmi remote management | Sunmi DMP with remote desktop control, annotation, file transfer [verified]; devices also manageable via Esper/Intune/Scalefusion [verified] | A free-ish fallback fleet tool tied to one vendor |
| PAX | Neptune Lite API (NeptuneLiteUser.getInstance().getDal(ctx)); APKs must be signed and distributed through PAXSTORE, security-reviewed, and whitelisted in the acquirer's tenancy [verified that PAXSTORE performs signing + security checks] | You usually cannot sideload on a production PAX estate. Expect 2–6 months of BD per acquirer [estimate]. Prefer Sunmi + a separate acquirer terminal in v1 |
| No GMS (EDLA-less SKUs) | No FCM, no Play Store, frozen WebView, no Play Integrity (r01 §1.3) | You need your own MQTT foreground service (r01 costs it at 1.0 eng-mo) and your own OTA channel. Never put a WebView on the critical path of a non-GMS device |
| Device Owner | Silent APK install, setLockTaskPackages/startLockTask, setLockTaskFeatures, setSystemUpdatePolicy(TYPE_INSTALL_WINDOWED) all require Device Owner, provisionable only at factory-reset time (QR at the setup wizard, NFC bump, or zero-touch) [known] | The install runbook is load-bearing. A "normally set up" device is permanently second-class |
| AOSP quirks | Stale tzdata/ICU on old ROMs (DST bugs in Europe/Kyiv, Europe/Warsaw); OEM battery killers terminating foreground services; missing PackageInstaller UI; some AIOs report the wrong Configuration.orientation on a landscape-locked panel [known] | Add a tz-data sanity check at boot and a server-authoritative business-day boundary (ties to r04 §4.2 clock skew) |
| RAM | Entry AIOs ship with 2 GB (e.g. Sunmi D3 Mini base) [estimate — confirm SKU] | Hard PSS budget of ~250 MB. onTrimMemory must drop image caches. A process kill mid-order is a data-loss event unless every keystroke is journaled to SQLite |
#3. Distribution
#3.1 Channel matrix
| Channel | Platform | Review latency | Rollout control | Rollback | Use it for |
|---|---|---|---|---|---|
| App Store (public) | iOS/iPadOS | 90% < 24 h; complex apps 2–5 days; expedited ~24 h [verified] | Phased release for automatic updates: 1/2/5/10/20/50/100% over 7 days, pausable up to 30 days [known] | None | Owner app, guest app (if you ship one) |
| Custom App via Apple Business Manager | iOS/iPadOS | Same App Review, typically 1–2 days [verified] | Assigned by MDM or redemption code; can be free or priced | None | The merchant POS app, if you ever ship iPad |
| TestFlight | iOS | Internal builds ~instant after the first external review of a version; 90-day expiry, 10k testers [known] | Manual group targeting | Just don't promote | Beta sites, internal |
| Ad Hoc | iOS | None | 100 devices/yr/type, 1-year profile expiry [known] | n/a | Device lab only |
| Apple Developer Enterprise Program | iOS | None | USD 299/yr | n/a | Do not use for customers. It is for your own employees. Misuse gets the cert revoked and every app on every device stops launching instantly. This has happened publicly to multiple companies [known] |
| Managed Google Play (private app) | Android GMS | Minutes to a few hours [known] | Percentage staged rollout, halt supported | Halt + forward-fix | Merchant app on GMS/EDLA devices |
| MDM-pushed APK | Android (Device Owner) | None | Your own cohorts, your own logic | Blocked by Android (§3.3) | The primary channel for POS/KDS/CFD |
| PAXSTORE | PAX terminals | Vendor security review, days–weeks [estimate] | Acquirer tenancy controls | Vendor-mediated | Only if you commit to PAX |
| Sunmi App Store / DMP | Sunmi | Vendor review [estimate] | Vendor console | Vendor-mediated | Fallback |
| Direct sideload | Android non-Device-Owner | None | Manual, requires user tap | Manual | Never in production |
#3.2 The Friday-night P1, with a clock
Scenario: 19:40 on a Friday, a build shipped Thursday turns out to drop every third kitchen ticket at sites with two printers. 180 sites, mid-service.
| Path | What actually happens |
|---|---|
| Server-side kill switch (correct answer) | 19:42 flip a flag → clients fall back to the previous print-routing code path on the next order. Time to mitigation: 2 minutes. Requires the flag to have existed before the incident and to be evaluated locally from a cached config, because half the sites' internet is flaky |
| Android: push previous APK via MDM | Blocked. INSTALL_FAILED_VERSION_DOWNGRADE. Uninstall-and-reinstall wipes app-private storage → destroys the unsynced order journal. Not an option during service. |
| Android: forward-fix | Build + test + sign + stage: realistically 2–5 h even with everything ready. Ships at ~01:00 |
| iOS: remove from sale | Stops new installs. Does nothing for the 180 iPads that already auto-updated. No rollback exists. |
| iOS: expedited review of a fix | ~24 h typical [verified]; occasionally faster if you plead. Ships Saturday |
Rules that follow, non-negotiable:
- Every risky code path ships behind a remotely-flippable flag with a locally-cached default, and the flag store must work when the site's WAN is down (last-known-good config persisted, TTL 7 days).
- Release freeze Thursday 12:00 → Monday 10:00. No exceptions, including "it's just a copy change".
- Staged rollout cohorts: internal lab (1) → 3 friendly sites → 10% → 40% → 100%, with a minimum 24 h soak spanning at least one dinner service at each step. Never advance on Fri/Sat.
- Forced-update policy must degrade, not block. A server returning
426 Upgrade Requiredthat hard-blocks the till at 20:00 is an outage you caused. Correct behaviour: client belowmin_supportedenters grace mode — local-only order entry and printing continue, sync is paused, a non-modal banner appears, and hard-block only triggers aftermin_supported_hard(set weeks later) and only outside the site's configured service hours. - Version skew is permanent. A device offline for 3 weeks (r04 §4.6) will rejoin on an old client. The sync protocol must be N-2 compatible in both directions, with a schema version negotiated per connection.
#3.3 The Android rollback trap, spelled out
- Android refuses in-place downgrades of a signed app (
INSTALL_FAILED_VERSION_DOWNGRADE). PackageInstallerrollback (SessionParams.setEnableRollback) requires system/privileged context — not available to a normal Device Owner app for arbitrary APKs[known — verify against current AOSP for your target API level].- Uninstall/reinstall wipes
/data/data/<pkg>— the SQLite journal of unsynced orders.
Mitigations, in order of value:
- Feature flags + kill switches (as above). This is 90% of the answer.
- Store the event journal where a reinstall cannot destroy it: a second copy in app-scoped external storage or a
BackupAgent-eligible location, encrypted with a Keystore-held key that is escrowed server-side per device. Costs ~0.4 eng-months and turns a catastrophe into an inconvenience. - Keep the previous APK cached on-device so a reinstall is offline-capable.
- Ship a recovery build channel: a tiny, rarely-changed app that can (a) export the local journal to a file, (b) fetch and install a specified version, (c) display support contact and a device ID. Installed at provisioning, updated twice a year.
#4. Fleet management
#4.1 MDM options and real prices
| Product | Platforms | List price [verified unless noted] | Minimums | POS-specific strengths | Weaknesses |
|---|---|---|---|---|---|
| Mosyle Business Premium | Apple | USD 1/device/mo, annual, 30-license min | 30 | Cheapest credible Apple MDM by a wide margin; free tier exists | Apple-only; support is thin at this price |
| Hexnode UEM | Apple, Android, Windows | USD 1 / 1.8 / 2.8 / 3.8 / 5.8 per device/mo by tier, annual | Varies | Cross-platform in one console; good kiosk/COSU support | Console is dense; Android dedicated-device features less deep than Esper |
| Esper | Android (dedicated), some Apple | USD 2 (Genesis) / 4 (Bridge) / 6 (Architect) per device/mo, 25-device minimum, 30-day trial | 25 | Built for POS/kiosk fleets. Remote view/control, staged app rollout pipelines, custom AOSP support, PCI DSS validated as a cloud provider for Android mPOS | Priced at 2–6× Mosyle; overkill below ~200 sites |
| Jamf Pro | Apple | USD ~4 (small business) / 5.75 (mobile) / 12.50 (Mac) per device/mo; enterprise 3.67–12.50 | Varies | Deepest Apple feature set, best ecosystem | Expensive per till; you need ~5% of its features |
| Iru (formerly Kandji) | Apple + Windows + Android since Oct 2025 rebrand | ~USD 30–60 per iOS device/yr; mostly custom quotes | Custom | Polished; now cross-platform | Opaque pricing; the product changed shape in the rebrand [verified] |
| Scalefusion | Android, Apple, Windows | ~USD 2–5/device/mo [known] | Varies | Strong COSU/kiosk story, India/EU presence, cheap | Less DevOps-ish than Esper |
| Sunmi DMP | Sunmi only | Bundled / low [estimate] | — | Remote desktop, annotation, file transfer, works on non-GMS Sunmi [verified] | Vendor lock; useless for iPads or Elo |
#4.2 Recommendation and the cost math
A reference single site runs 6 devices: 1 till AIO, 1 KDS, 1 handheld, 1 CFD (often the till's second screen — count it as 0), 1 spare/manager tablet, 1 kiosk (only in QSR). Realistically 4–6 managed endpoints.
| Strategy | Cost/site/mo | Comment |
|---|---|---|
| Esper Bridge on everything, 6 devices | USD 24 ≈ EUR 22 | 17–28% of a EUR 79–129 subscription. Only defensible with kiosks or >200 sites |
| Hexnode Pro on everything, 6 devices | USD 10.8 ≈ EUR 10 | Reasonable |
| Mosyle/Hexnode base tier for enrolment + kiosk lock + OS update window only, everything else in our own agent | USD 6 ≈ EUR 5.5 | Recommended. |
| Build it all ourselves | EUR 0 recurring, +2.0 eng-months (r01 item 10) and permanent ownership | Only if MDM cost genuinely threatens gross margin at scale |
The split that works: MDM owns what only the OS can do — enrolment, supervision, lock-task/kiosk, OS update windows, network/Wi-Fi payloads, remote wipe, lost mode. Your own in-app agent owns everything else — app version, remote config, feature flags, log capture, printer health, telemetry, "send diagnostics", remote restart of the app, sync status. Your agent works offline against a cached config; an MDM does not. Your agent knows what a "printer" and an "unsynced order" are; an MDM does not.
#4.3 Zero-touch provisioning: 6 devices in under 25 minutes
Target: a non-technical installer (or the restaurant owner, from a box shipped by courier) brings a site live without a phone call.
Android (the primary path):
- Devices are shipped pre-registered. Either (a) the reseller enrols them in the Android zero-touch portal (GMS/EDLA SKUs only), or (b) we ship a printed QR provisioning card in the box. The QR encodes:
{ "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "com.rb.dpc/.AdminReceiver", "android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM": "<base64url SHA-256 of signing cert>", "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION": "https://ota.rb.example/dpc-<ver>.apk", "android.app.extra.PROVISIONING_WIFI_SSID": "<site ssid>", "android.app.extra.PROVISIONING_WIFI_PASSWORD": "<key>", "android.app.extra.PROVISIONING_SKIP_ENCRYPTION": false, "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE": { "site_claim": "K7M-4T2", "role": "TILL" } }[known — key names are stable across API 26+; verify checksum encoding] - Installer powers on, taps the welcome screen 6× → QR scanner opens → scans the card. Device joins Wi-Fi, downloads the DPC, becomes Device Owner. ~4 min/device, mostly download.
- DPC applies policy (lock task, disable factory reset, disable USB debugging,
setSystemUpdatePolicywindow 03:00–05:00) and silently installs our app. - Our app launches, reads
site_claimfrom the admin extras, calls home, and self-assigns its role (TILL / KDS / HANDHELD / CFD / KIOSK) from the pre-built site plan in the back office. - Printer discovery wizard runs (r01 §2.6): mDNS + Epson UDP 3289 + ARP sweep → installer confirms which physical printer is "KITCHEN" via a test print that prints the station name in 48 pt → assign static IP + record MAC → print and stick a physical label.
- Menu + staff + tax config sync down. Site is live.
Timing [estimate]: 4 min/device × 6 = 24 min of device time, ~8 min of which is parallelizable; printer wizard 6–10 min; total 25–35 min for a competent installer, 60–90 min for a first-timer following a printed runbook. Budget 1.0 eng-month for the wizard + runbook (also counted in r01 item 12).
Apple (if/when iPads exist) — the purchasing constraint that surprises people:
- Automated Device Enrollment requires the device to be in your Apple Business Manager org, which requires purchase from Apple or an Apple Authorized Reseller who links the order to your ABM Organization ID.
[known] - A device bought from a random EU webshop is not in ABM. Your only fallback is Apple Configurator over USB on a Mac — ~2 min/device and it grants the user a 30-day provisional window in which they can un-enrol.
[known] - Therefore: iPad procurement is an ABM-linked purchasing decision, not a shopping decision. Write it into the reseller agreement or you will be manually configuring iPads forever.
#4.4 Remote diagnostics and screenshare
| Capability | Android | iPadOS |
|---|---|---|
| Remote view (see the screen) | Esper, Scalefusion, Sunmi DMP [verified] | Not possible. No third-party or MDM screen mirroring exists |
| Remote control (drive the UI) | Esper / Sunmi DMP [verified] | Not possible |
| Screenshot on demand | Via MDM agent or your own app with MediaProjection (needs a one-time user consent) | Not possible |
| Practical substitute | — | In-app "Record 15 s and attach to ticket" using ReplayKit (user confirms), plus a diagnostics bundle button |
| Log pull | Your agent uploads a rotating ring buffer (last 5,000 events, ~2 MB gz) on demand or on crash | Same |
| Remote restart of the app | Device Owner can force-stop + relaunch | No |
| Remote reboot | Device Owner reboot() | MDM RestartDevice command on supervised devices [known] |
Design consequence: because iOS remote control does not exist and Android remote control costs Esper money, the product must be self-diagnosing. Ship a Device Health screen on every device — printers with green/amber/red, last sync time, unsynced count, WAN/LAN status, app version, storage free, battery/thermal — plus a single "Send diagnostics to support" button that bundles logs + config + last 50 orders' metadata and returns a 6-character reference the staff member reads over the phone. This one screen removes an estimated 30–40% of support call time [estimate].
#4.5 Telemetry, offline recovery, lost/stolen
Telemetry event set (v1, deliberately small):
| Signal | Cadence | Payload |
|---|---|---|
| Heartbeat | 60 s | device_id, app_ver, battery %, thermal state, free disk, DB size, unsynced_count, LAN/WAN state, active role |
| Printer health | 30 s per printer | reachable, ASB status byte, paper state, last successful job ts |
| Perf | per session | cold start ms, p50/p95/p99 frame time (AndroidX JankStats), ANR count |
| Business-critical | per event | order sent, ticket printed, payment attempt/result, sync conflict resolved, forced logout |
| Crash | on crash | Sentry (Team plan ~USD 26/mo for 50k errors [known]) or self-hosted |
Volume: 6 devices × 1,440 heartbeats/day × ~200 B ≈ 1.7 MB/site/day → ~50 MB/site/month. At 1,000 sites that is 50 GB/month ingested — trivial cost, but compress and batch on the device, because r01 §4.4 notes CEE sites often run on LTE backup during outages where every megabyte is metered.
Offline recovery — the "site is dark for 3 days" plan:
- The app must boot, open a table, take an order, print, take cash, and print a Z report with zero network. This is the r04 requirement restated as a client requirement.
- Config, menu, tax rates, staff PINs, and feature flags are all persisted with a last-known-good copy and a 7–30 day TTL that warns rather than fails.
- Ship a physical recovery card in the install kit: support phone number, site ID, device IDs, and an offline manager override code (HMAC of site_id + date, verifiable offline) so a manager can void/refund without a server.
- The recovery build channel (§3.3) is the last resort.
Lost / stolen:
- Handhelds are the realistic loss vector, not 15" AIOs.
- Controls: PIN-on-wake after 60 s idle; full-disk encryption (default on modern Android/iOS); device-bound key in Keystore/Secure Enclave so a cloned APK on another device cannot sync; server-side device certificate revocation that takes effect on next connect; MDM lost-mode/lock/wipe.
- Because we hold no card data (r01 PCI posture — payment lives on the terminal), a stolen handheld is a hardware loss (EUR 400–600) plus a GDPR question about the guest data cached on it. Cap the on-device guest cache: today's orders + loyalty balances for guests seen today, nothing historical. This is also the r06 §6.6 DSAR answer.
#4.6 The white-label trap
Do not ship a per-restaurant branded native app for SMB. Apple Guideline 4.2.6 rejects apps produced from a template/generation service unless submitted by the client from their own account [known], Google is increasingly similar in practice, and 500 App Store listings is 500 review queues, 500 sets of screenshots, and 500 places a P1 can hide. Correct shape: one multi-tenant guest PWA under the restaurant's own domain (order.restaurantname.pl, white-labellable in CSS and logo), plus Apple/Google Wallet passes for loyalty (which are per-brand and cost nothing to mint). Reserve native guest apps for chains with 5+ sites who will fund their own developer account.
#5. POS UX constraints engineers underestimate
#5.1 Latency budget (the one that decides whether staff like you)
| Interaction | Budget | How it is achieved |
|---|---|---|
| Finger down → visual acknowledgement | ≤ 50 ms | Synchronous, local, no recomposition of the grid, no state round-trip. Use Modifier.pointerInput press feedback rather than waiting on the default ripple's async path |
| Tap → item appears on the ticket | ≤ 100 ms | Local in-memory append; SQLite write is fire-and-forget on a background dispatcher but must complete before the next frame's commit for durability |
| "Send to kitchen" → confirmation | ≤ 200 ms | Persist + enqueue the print job locally. Never wait on the network or the printer. The printer result arrives asynchronously |
| Payment start → terminal wakes | ≤ 800 ms | Vendor SDK bound |
| Cold start → interactive | ≤ 1.5 s on a 2 GB RK3568 AIO | Baseline Profiles + Startup Profiles (typically ~20–30% improvement [known]), R8 full mode, no work in Application.onCreate beyond DI graph, lazy menu image decode |
| Frame time under a rush | p99 ≤ 16.7 ms (60 Hz); ≤ 8.3 ms on 120 Hz iPad | LazyVerticalGrid with stable key + contentType, derivedStateOf, no animateContentSize in the grid, images pre-decoded at exact display size |
The classic killer: a 4000 px menu photo decoded at scroll time costs 60–120 ms of main-thread work and produces a visible stall exactly when the queue is longest. Pre-resize every menu image server-side to the exact device pixel sizes you ship (e.g. 240 px and 480 px squares) and cache them on disk at provisioning. Ship p99 frame time as telemetry so this regresses loudly, not quietly.
#5.2 The physical environment
| Factor | Rule with a number |
|---|---|
| Wet / gloved fingers | Capacitive touch degrades with a water film; nitrile and latex gloves are fine, thick vinyl is marginal. Specify touch targets in millimetres, not dp/pt, minimum 12 × 12 mm with ≥ 3 mm spacing. Compute per device: Sunmi D3 Pro 15.6" 1920×1080 ≈ 141 ppi → 12 mm = 67 px; D3 Mini 10.1" 1280×800 ≈ 149 ppi → 70 px; iPad 11" @264 ppi → 125 px = 62 pt. Material's 48 dp default is not enough on a 15" panel |
| Gestures | No swipe-to-delete as the only path (wet swipes misfire). No long-press as the only path. No drag-and-drop anywhere on the POS. Every destructive action needs an explicit button and a confirm |
| Glare | Typical Android POS panels are 250–400 nits (r01 §1.4). Do not default to a dark theme — under specular glare a dark UI competes with the reflected room and loses. Use a high-luminance background, ≥ 7:1 text contrast, body text ≥ 16 sp, ticket lines ≥ 20 sp, money in tabular figures so columns align |
| One-handed | On a 5–6" handheld the server's other hand is holding plates. Put Send/Fire/Pay in the bottom 25% of the screen, never the top-right. Ship a left/right-hand mode toggle. Assume the device is held at 45° in poor light |
| 8-hour shifts | Cap all animation at 150 ms. No parallax, no springy overscroll, no confetti. One distinct "sent" chime, off by default in fine dining. Never buy OLED for a KDS — a static ticket header burns in within months. Handhelds die at ~5 h under continuous use [estimate]: spec 2 batteries or a hot-swap SKU per handheld |
| Mounting | KDS above the pass is read at ~2 m. Rule of thumb: cap height ≥ viewing_distance/200 → 10 mm cap height at 2 m. On a 21.5" 1920×1080 (≈102 ppi, 0.248 mm pitch) that is ~40 px cap height ≈ 50–60 px font size for item names. If your KDS uses 24 px text, the kitchen will keep the printer |
#5.3 Train in 15 minutes
Make this a testable acceptance criterion, not an aspiration. A new hire who has never seen the product must, with a 90-second video and no trainer, complete in ≤ 15 minutes:
- Clock in with a 4-digit PIN.
- Open table 12, add 3 items including one with a required modifier group.
- Fire course 2.
- Split the bill 2 ways by item.
- Take a card payment and a cash payment with change.
- Void an item (with a manager PIN override).
- Clock out.
Run this test with actual restaurant staff, not engineers, at least once per quarter. Anything that fails it moves to the manager-only surface or the back office. Role-based UI is the mechanism: a server sees perhaps 20% of the app's functionality; a shift manager 60%; the owner uses the web console, not the till.
Why it matters commercially: hospitality staff turnover in the EU runs high (commonly cited at 70–100%/yr in the sector [estimate]), so training cost is a recurring cost the operator pays, and "the new kids can't work the till" is a genuine churn driver.
#5.4 Multi-language staff
- Language is a property of the user, not the device. In PL/CZ/DE the kitchen brigade frequently works in Ukrainian or Russian, the servers in the local language, the owner in a third (r01 §2.7).
- v1 language set for the CEE beachhead: uk, ru, pl, cs, ro, en, de, sk — 8 locales. Add es/it/fr/pt/nl for Western EU (13 total).
- Sizing: ~1,400–1,800 UI strings at v1
[estimate]; professional translation EUR 0.08–0.15/word; average 3 words/string → EUR 3,000–6,500 initial, EUR 400–900/month upkeep[estimate]. Machine translation with a native reviewer per locale halves this and is acceptable for a POS but not for guest-facing legal/consent copy (r06 §6). - Engineering rules: no string concatenation; ICU MessageFormat for plurals (Polish/Czech/Ukrainian have 3–4 plural forms and
one/few/many/othercategories that break naiveif (n==1)code); allow +40% text expansion in every button; German compound nouns will break your ticket layout. - Print language ≠ UI language ≠ guest language. Menu items need a per-station translation column (r01 §2.7) — a schema decision, not an architecture one.
- RTL (Arabic/Hebrew) is not needed for the beachhead. Defer, but use logical layout properties (
start/end) so it is a config change later, not a rewrite.
#5.5 Colour-blind-safe KDS
~8% of men and ~0.5% of women of Northern European descent have a colour-vision deficiency, predominantly deutan/protan [known]. A KDS that encodes "late" as red and "on time" as green is unreadable to one cook in twelve.
Rules:
- Never encode state in hue alone. Every ticket carries a numeric elapsed timer (mm:ss) as the primary signal.
- Add redundant channels: border weight (2 px → 6 px), position (late tickets sort left/top), and a fill pattern or an icon.
- If you use a colour ramp, use blue → amber → magenta, not green → amber → red. Both deutans and protans distinguish it.
- All text ≥ 4.5:1 contrast against its ticket background after the state colour is applied — the amber state is where teams fail this.
- Validate with a deutan/protan simulator in CI on screenshot tests; it's a 0.1 eng-month check that prevents a class of bug you will otherwise never hear about (cooks do not file tickets).
#5.6 Tip screens
- In a semi-integrated payment flow — which is what you should ship (r01) — the tip prompt is rendered by the payment terminal's own app (Adyen, SumUp, Viva, Nexi, PAX), not by yours. You configure the presets via the terminal API and read the tip amount back. You do not control that UI. Design accordingly: your CFD shows the total, the terminal shows the tip prompt, your ticket reconciles.
- Full control of the tip UI requires a fully-integrated flow or your app running on the terminal, both of which pull you into PCI scope. Not a v1 move.
- Beachhead reality: tipping is 5–10% in PL/CZ/UA and increasingly card-based; in DE/AT rounding-up is the norm; in FR/ES service is included. Default presets must be per-country, and CEE defaults should be lower than any US-derived library's.
- Do not pre-select a tip percentage and do not use a "no tip" option rendered smaller or greyer. Beyond the reputational issue, pre-ticked defaults are a live consumer-protection question in the EU
[estimate — worth 30 minutes with counsel]. - Tips have fiscal and payroll consequences (r03 §4). The client's job is to record
tip_amount,tip_method, andtip_recipient; the allocation policy (pooled vs individual) is back-office configuration.
#5.7 Accessibility — where it is legally binding
| Surface | Regime | Requirement |
|---|---|---|
| Guest web ordering, QR ordering | EU Accessibility Act (Directive (EU) 2019/882), applicable from 28 June 2025. E-commerce services are in scope; conformance is via EN 301 549 → effectively WCAG 2.1 AA [known] | Real obligation. There is a microenterprise exemption for services (<10 staff and ≤ EUR 2M turnover) but it protects the café, not us, if we are the service provider. Get counsel to confirm who the "service provider" is in a white-label ordering page |
| Self-order kiosk | EAA covers self-service terminals including payment terminals [known]; US: ADA Title III | Tactile/audio mode, 3.5 mm headphone jack, screen-reader output, reach range (US ADA §308: 380–1220 mm side reach), high-contrast mode, no colour-only cues. This adds ~1.0–1.5 eng-months to the kiosk and constrains the hardware you may resell |
| Guest website (US) | ADA Title III litigation; WCAG 2.1 AA is the de facto standard | A real, insurable litigation cost in the US market |
| Staff POS / KDS | Not consumer-facing; no direct EAA obligation | But employment-law reasonable-accommodation applies. Ship: adjustable text scale (100/125/150%), the colour-blind-safe palette (§5.5), and no audio-only alerts |
| Back-office console | Not in scope | Do WCAG AA anyway; it costs ~5% extra if designed in and 10× if retrofitted |
#6. Cross-cutting engineering decisions
| Decision | Call | Why |
|---|---|---|
| Design system | One token set (colour, spacing, type, elevation) with two component libraries — Compose for counter, React for web — generated from the same JSON tokens (Style Dictionary or similar) | Two libraries is the honest cost; shared tokens keeps them from diverging. 2.0 eng-months |
| State management (Compose) | Unidirectional; one StateFlow<ScreenState> per screen; no ViewModel holding the order aggregate — the aggregate lives in :domain and is the same object the sync engine folds (r04 §3.4) | Prevents the classic bug where the UI and the event log disagree |
| Feature flags | Server-defined, locally cached with last-known-good, evaluated synchronously offline, with a per-site override | The Friday-night mitigation path (§3.2) |
| Crash + perf | Sentry (or self-hosted GlitchTip) + AndroidX JankStats + custom cold-start trace | Cheap; the alternative is guessing |
| Testing | Compose UI tests on emulators for logic; a physical device lab is mandatory for the AIDL/printer/terminal paths (r01: EUR 6–10k) | Device quirks are not emulatable |
| QA shape | 1 QA engineer who owns the device lab from day one, plus 20 hours/quarter of engineers working actual shifts in a customer's restaurant | The single highest-ROI practice in this category. Every non-obvious requirement in §5 came from a shift, not a spec |
| Kiosk lockdown (Android) | Device Owner + setLockTaskPackages + setLockTaskFeatures(HOME | NOTIFICATIONS off | GLOBAL_ACTIONS off | KEYGUARD off | SYSTEM_INFO on) + setSystemUpdatePolicy(TYPE_INSTALL_WINDOWED, 03:00–05:00) | The update window is the one people forget. Without it Android reboots a till at 20:15 |
| Second screen (CFD) | DisplayManager + Presentation in the same process as the till app | Sharing the process means zero IPC and zero sync problem; the cost is that a crash takes both screens |
#Build effort
Assumes Kotlin/Compose for counter surfaces and React/TS for web, per §2.4. Excludes the offline/sync core (r04), the printing stack and hardware certification (r01), fiscal drivers (r03), CRM/loyalty backend (r06), payments integration/certification, and all backend services. AI assistance is assumed and already baked in at roughly 1.5× on web/CRUD, 1.15× on native/device/perf work, 1.0× on UX decisions and on-site debugging [estimate] — AI is close to useless for "the Sunmi service unbinds after 4 hours idle".
| # | Work item | Eng-months | Variance drivers |
|---|---|---|---|
| 1 | Design system + tokens + Compose and React component libraries | 2.0 | Two renderers is the floor. A designer with POS experience halves the rework; without one, add 1.0 |
| 2 | POS terminal app — shell: auth, PIN, roles/permissions, device pairing + role self-assign, settings, i18n scaffolding | 1.5 | Role model complexity; 8 locales adds ~0.3 |
| 3 | POS — menu grid + modifier engine UI + search | 2.0 | Nested/conditional modifier groups and combo/meal-deal UI can double this |
| 4 | POS — order/ticket: courses, seats, splits (by item/by amount/by seat), merges, table transfers, voids, comps, discounts | 3.0 | Splits and transfers are ~55% of it. Cutting them makes you unsellable to table service |
| 5 | POS — floor plan / table map + service-state colouring + editor in back office | 1.5 | A drag-to-place editor is 0.8 of it; a fixed grid is 0.4 |
| 6 | POS — payment UI: split tender, cash rounding, tips, partial refunds, receipt choice (print/email/QR), semi-integrated terminal state machine | 2.0 | Second acquirer +0.5; the terminal state machine (timeouts, "did it go through?", reversal) is the risk |
| 7 | POS — cash management: drawer counts, paid-in/out, X/Z, shift open/close, blind close, discrepancy reporting | 1.0 | — |
| 8 | POS — offline UX: degraded-mode banners, sync status, conflict surfacing, grace-mode forced-update behaviour, recovery flows | 1.0 | Depends on r04's model; the UI half is what's costed here |
| 9 | POS — perf, memory, and hardening pass on 2 GB devices | 1.5 | Baseline profiles, image pipeline, PSS budget, process-death journaling. Skipping this is how you get "it's slow on Friday" |
| 10 | Handheld waiter app | 3.0 | ~70% reuse of 2–8; the 30% is one-handed layout, thumb zones, battery/thermal, tableside payment, offline-first at the far end of the Wi-Fi |
| 11 | KDS + expo mode + bump bar | 3.0 (costed in r01 item 6) | A ticket list ships in 1.0; a KDS a chef accepts is 3.0 |
| 12 | Customer-facing display | 1.2 | Dual-screen Presentation + loyalty prompt + tip/total display + idle marketing loop |
| 13 | Back-office web console: menu management (3.0), staff/roles/labour (1.5), reporting + analytics (2.5), settings/devices/printers (1.5), multi-site switcher (1.0) | 10.0 | Menu management is chronically underestimated: modifiers, availability schedules, per-channel pricing, per-station routing, multi-language, images |
| 14 | Owner mobile app (thin: today's sales, live tickets, alerts, remote void approval) | 1.5 | Push on iOS+Android; keep it read-only + approvals or it becomes a second POS |
| 15 | QR table-ordering PWA | 3.0 | Reuses #16; adds table binding, pay-at-table, split-by-item, staff notification, EAA/WCAG pass |
| 16 | Guest web ordering + loyalty wallet (menu render, cart, checkout, order tracking, account, Apple/Google Wallet pass, SEO/perf, WCAG 2.1 AA) | 7.0 | Delivery-mode logistics (slots, zones, fees) can add 2–3. Wallet passes alone are 0.8 |
| 17 | Self-order kiosk app | 4.5 | Plus 5–8 eng-months for unattended EMV certification (r01 item D), plus ~1.0–1.5 for EAA/ADA kiosk accessibility |
| 18 | Fleet agent (in-app): remote config, feature flags with LKG cache, OTA install for Device Owner, log ring buffer + upload, telemetry, device health screen, remote app restart | 3.0 | Overlaps r01 item 10 (2.0) — count ~1.5–2.0 net new here if r01 is already funded |
| 19 | Provisioning wizard + QR provisioning pipeline + installer runbook | 1.0 (in r01 item 12) | Apple ABM path adds 0.5 if iPads ship |
| 20 | Internal support console: read-only site impersonation, device inspector, log fetch, order replay, receipt reissue, audit trail | 2.0 | Needs a hard permission/audit model — you are looking at customers' financial data |
| 21 | Release engineering: signed build pipeline, cohort/staged rollout service, kill-switch service, version-skew compatibility tests, recovery build channel | 2.0 | Buying Esper Pipelines cuts ~0.7 at USD 4/device/mo |
| 22 | Accessibility + colour-blind CI checks + i18n plumbing across all surfaces | 1.5 | EAA scope for guest surfaces is the driver; retrofit costs ~5× |
| v1 sellable (T0 + T1: items 1–11, 13, 14, 18–21) | ≈ 38–44 → take 42 | ||
| + T2 (CFD, guest ordering, a11y sweep: 12, 16, 22) | +9.7 | ||
| + T3 (kiosk app only, excl. EMV cert: 17) | +4.5 | ||
| iPad target activated later (CMP iOS) | +6–10 | 15–25% of the counter-surface total, plus MFi/entitlement/ABM overhead | |
| If you choose RN or Flutter instead of Kotlin | +5.5 and +0.15–0.25/quarter forever | §2.3 |
#Minimum team shape
| Scenario | Shape | Calendar to v1 client surfaces |
|---|---|---|
| S1 — bootstrapped (founder + 4) | 2 Kotlin/Android (one owns POS, one owns KDS + devices + fleet), 1 full-stack TS (back office, then guest/QR), 1 backend/domain (r04), founder on product/design/sales. Plus a fractional product designer at ~0.5 FTE, EUR 2.5–4k/mo in UA/PL [estimate] — non-negotiable, a POS without a designer does not sell | ~2.2 effective FTE on client work → 42 eng-months ≈ 19 calendar months. That does not fit inside a 24-month runway once backend, printing, and fiscal are added. Honest S1 scope: POS + back office + KDS + printing + fleet agent, Android-only, one country — ≈ 26 eng-months ≈ 12 months calendar, leaving 12 months to sell. Handheld, guest ordering, CFD, kiosk are all year 2 |
| S2 — funded (EUR 5–10M) | POS pod (3 Kotlin + 1 designer), Web pod (3 TS + 1 designer), Guest pod (2 TS), Platform/devices pod (2 + 1 QA owning the hardware lab) = 13 product engineers + 2 designers + 1 QA. Plus a field-ops/installer hire by customer #10 | Full surface list (T0–T2) in 9–11 calendar months; kiosk + unattended EMV pushes to 15–18 |
Roles that are not optional at any funding level: (1) one QA engineer with a physical device lab; (2) one designer who has either worked on a POS or will work 20 restaurant shifts; (3) by customer #10, one field/support person who is not an engineer.
#Open questions / what would change this answer
- Beachhead = CEE or Western EU/US? This is the framework decision in disguise. CEE ⇒ Android AIO at EUR 465–688 dominates ⇒ Kotlin/Compose Android-first is unambiguously right and iPad may never be needed. US/Western EU ⇒ iPad attach rises materially ⇒ React Native becomes the safer pick and the +5.5-month bridging tax becomes the price of insurance. Resolve before the first commit. Verify by asking 15 target restaurants in the beachhead what device they'd expect on the counter and what they'd pay for it.
- Is Compose Multiplatform on iOS good enough for a POS in 2026? JetBrains marks the UI stable for Android/iOS/desktop as of May 2026
[verified], but "stable" and "a server will not notice" are different bars — text input, scroll physics, and accessibility on iOS are the areas to test. Verify: build the item-grid + ticket screen in CMP, run it on a real iPad for a week, measure p99 frame time and have three servers use it. Two weeks of work that de-risks a EUR 500k decision. - Flutter's Impeller on the exact AOSP GPUs we resell. Impeller is the only renderer on modern Android as of 2026
[verified]; Rockchip RK3568 / Mediatek GPU drivers on EUR 465 POS AIOs are not a well-trodden path[estimate]. Only matters if question 1 pushes you toward Flutter. Verify: one afternoon with a Flutter sample on a D3 Mini and an Elo I-Series 4. - Do we sell through acquirers on PAX/Ingenico estates? If yes, PAXSTORE distribution and acquirer whitelisting become a 2–6 month business-development dependency
[estimate]that gates your entire release process, and sideloading stops being an option. If no, Sunmi + a separate terminal keeps you in control. Verify: one conversation each with a PL and a UA acquirer about third-party app onboarding, before designing the release pipeline. - EAA scope: who is the "service provider" for a white-label ordering page? If it's us, the microenterprise exemption never applies and every guest surface must be EN 301 549 conformant from day one (+1.5 eng-months and an ongoing audit obligation). If it's the restaurant, most single-site cafés are exempt and we can ship a lighter v1. Verify with counsel — this is a 2-hour question with a 6-figure answer.
- MDM: build or buy, and at what scale does it flip? At 6 devices/site, Esper is EUR ~22/site/mo against a EUR 79–129 subscription. Our own agent costs ~2–3 eng-months up front. The flip point is roughly 150–250 sites
[estimate]. Verify: get real quotes from Esper, Hexnode, and Scalefusion at 500 and 2,000 device commitments — published list prices are not what you'll pay.