Build interactive event flows in a browser, run them fully offline on locked-down kiosk hardware, and sync captured data back when a connection returns.
Build an interactive event flow in a browser, publish it, and it runs on a locked-down kiosk out on the floor. The kiosk works with no network at all: it captures every entry to a local database first, drives attached hardware like a relay or a receipt printer, and syncs the data back to the cloud when a connection returns. Nothing is lost if the wifi drops, the machine reboots, or the power cuts mid-entry.
It is three services. A cloud console where you build flows and manage the fleet, a sync server that is the source of truth, and the kiosk app that runs offline on the hardware.
- An operator builds a flow in the console: a sequence of screens (welcome, form, quiz, thank-you) with fields and optional hardware actions, then publishes a version.
- A kiosk is provisioned from the console. It gets its own credentials and pairs to an event.
- On the floor the kiosk pulls its published flow and runs it fullscreen, offline. A visitor taps through the screens on a touch display.
- Each submission is written to a local SQLite database inside one transaction, and only then does the screen advance. The write is flushed to disk before the visitor sees the next screen.
- Any hardware action on the screen fires after that durable write: pulse a relay, print a ticket, read a sensor. A hardware fault never loses the capture.
- A background sync engine drains the local outbox to the cloud whenever it can reach the server, retrying with backoff and deduplicating so a dropped connection never doubles a record or loses one.
- Back in the console, the fleet view shows each kiosk's status, backlog, and storage, and captured data can be reviewed or exported to CSV.
Flow builder. Author a flow as a list of screens with typed fields (text, email, phone, number, select, multiselect, boolean, rating). Attach hardware actions to a screen, set a theme color, and watch a live kiosk preview render exactly what will run on the device. Flows are versioned and immutable once published.
Kiosk runtime. The published flow runs fullscreen with large touch targets, an on-screen keyboard for text fields, inline validation, and an idle reset that wipes entered data and returns to the start. The only ambient signal is a small sync badge; everything else is the visitor's flow.
Offline-first capture. Every entry lands in a local SQLite database with write-ahead logging and full synchronous durability. Captures survive an unclean shutdown or power loss because the row is committed to disk before the flow advances. A transactional outbox tracks what still needs to sync.
Device fleet. Provision a kiosk from the console and it is issued its own machine credentials through Logto. The fleet view shows derived online/offline status, pending backlog, storage use, and power source per device, with health charts for backlog, storage, and heartbeat gaps over time.
One-time pairing. Provisioning shows a client secret exactly once, with copy buttons and pairing steps. Enter it on the kiosk's admin screen and it pulls its flow and starts running.
Hardware over serial. A framed, byte-stuffed serial protocol drives relays, sensors, printers, and scanners. Replies are matched to commands, NACKs and timeouts retry with backoff, and a dropped port reconnects on its own. The driver is proven against a real virtual serial loopback, not a mock.
Locked-down operation. In the packaged build the kiosk is a frameless, non-closable fullscreen window with developer tools, refresh, navigation, and OS shortcuts blocked. A hidden corner gesture and a PIN open a technician panel for status, sync, hardware, config, and a controlled exit.
| Layer | Choice |
|---|---|
| Server | Bun + Hono, three-layer routes/controllers/services, Prisma over Postgres |
| Console | React + Vite, TanStack Query, Tailwind, Recharts for health charts |
| Kiosk | Electron + electron-vite, React renderer, a hardened contextBridge IPC boundary |
| Local store | better-sqlite3 with WAL and full synchronous durability, a transactional outbox |
| Hardware | serialport over a framed protocol, proven on a real serial loopback |
| Auth | Self-hosted Logto: OIDC auth-code for admins, OAuth2 client-credentials for devices |
| Sync | Hand-rolled outbox: UUIDv7 ids, idempotency keys, per-record acks, backoff with jitter |
| Packaging | electron-builder (Windows NSIS, Linux AppImage and deb), electron-updater |
| Testing | Bun test, Vitest, Testing Library, Playwright, real-hardware and real-Postgres integration |
- Docker and Docker Compose
- Bun 1.3+
- Node 20+ for the Electron kiosk build
Bring up Postgres, Logto, and the server with one command:
cp .env.example .env
docker compose up -dThis starts:
- Postgres on
5432with two databases,kioskfor the app andlogtofor auth - Logto on
3001(OIDC) and3002(admin console) - The server on
8787
Check health once the containers report healthy:
curl http://localhost:8787/healthLogto needs two applications configured once, in its admin console at http://localhost:3002:
- Create the admin account on first visit.
- Create an API resource with the indicator from
LOGTO_API_RESOURCE(defaulthttps://api.kiosk-engine.local). - Create a Single Page App for the console. Set the redirect URI to
http://localhost:5173/callbackand post-logout tohttp://localhost:5173. Copy its App ID intoVITE_LOGTO_APP_IDin.env. - Machine-to-machine apps for devices are created later, per device, from the console.
cd console
bun install
bun run devOpens on http://localhost:5173 and redirects to Logto to sign in.
cd kiosk
bun install
bun run devThe kiosk captures locally with no server. Pair it to an event from its admin screen using the credentials shown when you provision the device in the console, and it will pull its flow and start syncing.
Each service has its own suite. From a service directory:
bun run typecheck
bun run test
bun run test:e2eThe kiosk also has a real serial-hardware suite (bun run test:hw) that runs against a virtual serial loopback. See tools/README.md for the loopback setup on Windows (com0com) and Linux (socat).






