←

ePOS: QR point of sale

A point of sale for event stalls: upload a spreadsheet of stock, print one QR per item, scan sales on a phone and watch stock and revenue update, even when the network drops.

Scan, sell, sync later.

A point of sale for event stalls: upload a spreadsheet of stock, print one QR per item, scan sales on a phone and watch stock and revenue update, even when the network drops.

used at events · Next.js 14 · Postgres · Offline-first

  • Nov 2025Built and used
  • 0Duplicate sales, by design
  • 30 sBackground sync interval
  • 5Retries per sale, with backoff
A printed QR sheet: one code per item, each holding only the item ID
A printed QR sheet: one code per item, each holding only the item ID

Product developer

Built for stalls selling plush toys at events, where the Wi-Fi is the first thing to fail. The phone records every sale locally first and the server is made so a retry can never count a sale twice.

  • Spreadsheet upload, CSV or XLSX, that updates existing items and creates new ones
  • Printable QR sheets generated on the server
  • A phone scanner page that logs each scan as a sale
  • Dashboard for sold, remaining and revenue
  • The offline-first layer: local storage, sync engine and idempotent API

A sale, offline-first

Every scan is safe the moment it happens, and the server sorts out the rest when the network returns.

  1. 1Scan the QR
  2. 2Save the sale on the phone
  3. 3Queue as pending
  4. 4Sync in batches of ten
  5. 5Server checks the client ID
  6. 6Stock and revenue update
  7. 7Marked synced

What it does

  1. 01

    Upload stock

    Drop a CSV or XLSX of items; existing ones update, new ones are created.

  2. 02

    QR sheets

    One printable QR per item type, encoding only its ID.

  3. 03

    Phone scanner

    A camera page on any phone, scanning through WebAssembly for speed.

  4. 04

    Live dashboard

    Sold and remaining per item, and revenue totals.

  5. 05

    Works offline

    Sales are stored locally with a status of pending, syncing, synced or failed, and the UI shows the queue.

How it was built

  1. Local storage layer

    IndexedDB through idb, with atomic transactions, corruption recovery, status queries and health checks.

  2. Sync engine

    Background sync every 30 seconds when online, in batches of ten, retrying each sale up to five times at 5, 10, 20, 40 and 80 seconds with jitter, plus a manual sync button.

  3. Idempotent API

    Each sale carries a client ID with a unique constraint. The first request creates the sale and moves stock, a duplicate returns the existing sale, and a race returns a conflict, so unlimited retries never double count.

Stack

App
Next.js 14 App Router, TypeScript, Tailwind CSS.
Data
PostgreSQL with Prisma; IndexedDB on the phone.
QR
qrcode on the server, zbar-wasm in the browser.