Work
Ri-Scan Mobile
The offline-first field app behind Ri-Scan — barcode scanning, on-device SQLite, signature capture and a self-healing sync engine.
- Role
- Architecture & mobile · Flucite
- Year
- 2026
- Type
- Mobile
- Stack
- React Native · Expo · TypeScript · SQLite · Zustand

Overview
CIT crews scan every bag at every shop on their route, often in basements and loading bays with no signal. The app records everything locally first and streams it to the Ri-Scan backend whenever a connection exists, so the control panel stays live without the crew ever waiting on the network.
Offline-first storage
expo-sqlite in WAL mode with foreign keys enforced and a busy timeout. Sessions (route, crew, vehicle), shops and bags are relational tables, each row carrying a syncStatus; deletes go into a separate bag_deletes outbox so removals sync as reliably as inserts. Sessions get a client-generated UUID, so the server can identify them before they have ever been seen online. An on-device event log records every action and sync failure for later audit.
Sync engine
A single-flight sync pass drains the outboxes in dependency order — sessions, shops, bags, deletes — to the backend's live endpoints (session-started/closed, store-opened, bag-added, bag-removed). Passes are triggered every 5 seconds, on NetInfo reconnect and on app foreground; a call that arrives mid-pass sets a rerun flag instead of running concurrently. Each request has a 15-second AbortController timeout, rows are marked synced only on an explicit ok response, and failures are logged once per item and again when they recover, so the log stays readable.
Scanning & proof of handover
expo-camera reads Code 128, Code 39, EAN-13, EAN-8 and QR codes for shops and bags. Bags are typed (money, change, postal, voucher, plus delivery and free-text "other" entries with quick reasons), and each shop is closed with a signature drawn on a react-native-svg canvas via PanResponder and stored as Base64 with the shop record.
Native module & delivery
A custom Expo native module in Kotlin writes the end-of-day session log to the public Downloads folder through MediaStore (Android 10+), degrading silently on platforms without it. App state lives in Zustand, animations run on Reanimated, and builds are produced by EAS: merging a PR to main triggers a GitHub Actions workflow that builds the Android release.