{
  "id": 13270239,
  "title": "How I built a PWA barcode scanner that pairs your phone to a desktop spreadsheet",
  "url": "https://urgent.news/2026/10/10/how-i-built-a-pwa-barcode-scanner-that-pairs-your-phone-to-a-desktop",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-10-10T00:30:30.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/skycang/how-i-built-a-pwa-barcode-scanner-that-pairs-your-phone-to-a-desktop-spreadsheet-2eib"
  },
  "original_language": "en",
  "account": "Small retail teams often purchase expensive USB barcode scanners for each workstation or manually input codes. These scanners are tethered and priced per seat, while most people already own barcode readers in the form of their phone cameras. This led to the creation of ScanSheet, a Progressive Web App (PWA) built during nights and weekends. The following sections detail the engineering story behind ScanSheet.\n\nArchitecture:\nThe desktop browser tab hosts a table, and the phone camera opens its own browser tab. A QR code containing a session token appears on the desktop, which the phone scans. Both devices then connect via a WebSocket relay. Every scan is streamed in real-time to the desktop table, and quantities are automatically aggregated. The sheet can export data locally to CSV/XLSX files. The relay is intentionally simple, never persisting scan payloads. Scan data is stored only in the two browsers (IndexedDB + localStorage).\n\nDecoding:\nZXing, a browser-based library, is used for barcode decoding. The BarcodeMultiFormatReader class is instantiated, and then decodeFromVideoDevice is called with the necessary parameters. The result callback is used to send the scanned text to the server when a valid scan is detected.\n\nThe Relay:\nA simple WebSocket is set up with the following structure:\n1. Connection event listener\n2. Message event listener for handling incoming data\n3. Iterating over clients with the same session and sending the received data to them\n\nOptimization:\nTo improve accuracy and performance on low-end Android devices, only specific barcode formats (Code128, EAN-13, QR) are enabled instead of allowing all formats. Debouncing is also implemented to ignore duplicates within a 1.5-second window, reducing redundant scans.\n\nWebSocket and Data Persistence:\nThe relay is a straightforward WebSocket connection with no database involved. The phone queues scans in IndexedDB when disconnected and flushes them upon reconnection. This setup ensures the scanner never blocks, even during dead zones in a warehouse environment.\n\nService Worker Bug:\nA recurring issue faced during development involved the Service Worker constantly serving the old version of scripts to returning visitors. The solution involved:\n1. Serving the script with Cache-Control: no-store\n2. Bumping the Service Worker's cache name to purge old caches\n3. Adding a version query parameter to the script URL\n\nLessons Learned:\nCache-first strategies coupled with stable URLs can lead to outdated caches, causing issues in the application. It is crucial to version every mutable asset URL to prevent such problems.",
  "summary": "The problem Small retail teams either buy a $100+ USB barcode scanner per workstation or type codes by hand. The scanners work, but they are tethered and priced per seat. Meanwhile everybody already carries a barcode reader in their pocket: a phone camera. So the question became: what if the phone is the scanner, and the desktop browser is the sheet? That became ScanSheet, a PWA I have been…",
  "key_points": [
    "Desktop and phone connect via WebSocket relay, streaming real-time scan data to a table",
    "Optimizations include enabling specific barcode formats, debouncing, and versioning script URLs"
  ],
  "editors_take": "This development democratizes access to efficient inventory scanning for small retail teams by leveraging existing phone cameras and web technology, potentially disrupting the market for pricey, tethered USB scanners.",
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}