Barcode systems reduce human error by automating data entry, and scanning barcodes is faster than manual entry for receiving and picking processes, providing real-time inventory updates that allow for better decision-making. For Indonesian warehouses that still rely on handwritten receiving sheets and manual inventory cards, barcode scanning integration represents a step-change in accuracy and speed. At Commsult, I implemented barcode scanning for a manufacturing client's warehouse — integrating handheld scanners and mobile phone cameras with our custom ERP's inventory module. This post covers the full implementation from hardware choice to production deployment.
There are three hardware options for barcode scanning in an ERP warehouse integration: dedicated handheld barcode scanners (Zebra, Honeywell — rugged, fast, expensive at ~Rp 3-8M each), mobile phones with camera-based scanning (use the company's existing smartphones — free hardware but slower scanning), and fixed barcode readers at dock doors for receiving (Zebra fixed readers — ideal for high-volume receiving, Rp 15-30M each). For our client, we used a hybrid: fixed readers at the receiving dock and mobile phones with a PWA scanning app for warehouse staff. This balanced cost and performance.
We built a Progressive Web App specifically for warehouse operations — a mobile-first React application that runs in the browser without app store installation. The barcode scanning is implemented using QuaggaJS (1D barcodes like Code 128, EAN-13) and @zxing/browser (QR codes and 2D barcodes). The PWA prompts the user for camera permission on first use. Scanning opens the device camera and reads barcodes continuously — a successful scan triggers a haptic vibration and audio beep, then loads the scanned item's details. For environments where camera scanning is too slow (high volume receiving), we also support USB barcode scanners connected to a laptop running the same PWA.
Warehouse Barcode Scanning System
Hardware Layer:
┌───────────────────┐ ┌─────────────────────┐
│ Smartphone Camera │ │ Zebra ZT410 Printer │
│ (PWA scanning) │ │ → prints Code 128 │
│ QuaggaJS + zxing │ │ labels via ZPL │
└─────────┬─────────┘ └─────────────────────┘
│
▼ (offline-first via IndexedDB)
┌───────────────────────────────────────────────┐
│ React PWA (Cloudflare Pages) │
│ │
│ Scan → Lookup item → Confirm qty → Submit │
│ │
│ Offline mode: queue scans in IndexedDB │
│ Sync when online: POST /api/movements/batch │
└─────────────────────┬─────────────────────────┘
│ (online sync)
▼
┌───────────────────────────────────────────────┐
│ NestJS WarehouseModule │
│ │
│ POST /goods-receipts (GR scanning) │
│ POST /pick-orders/scan (picking) │
│ POST /cycle-count/submit (stocktake) │
│ │
│ → optimistic locking for concurrent scans │
│ → create_movement() PostgreSQL function │
└───────────────────────────────────────────────┘
Label format (Code 128 + GS1-128 for lots):
┌─────────────────────────────────────────────┐
│ ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ │
│ SKU: MAT-001234 Lot: L2025-0412 │
│ Bahan Baku A — 25kg │
│ Exp: 2026-04-12 │
└─────────────────────────────────────────────┘From my experience building ERP systems at Commsult: print your own barcodes on labels and attach them to your product packaging — don't rely on manufacturer barcodes. Manufacturer barcodes (GS1) often aren't in your ERP's item master, and scanning them returns no match. We print Code 128 barcodes encoding our internal item code (SKU) using ZPL (Zebra Programming Language) sent to a Zebra label printer. Each label has: barcode, item code, item name, and lot number. This takes 1 day to set up but saves hours of lookup time weekly.
The goods receipt (GR) scanning flow: warehouse staff receives a physical delivery, opens the GR screen on the PWA, selects the matching Purchase Order from a list of expected deliveries, and scans each item barcode. Each scan: looks up the item in the ERP, shows item name and expected quantity from the PO, prompts for quantity received (default 1, adjustable), and adds to the GR list. When all items are scanned, staff submits the GR — the NestJS backend creates the goods receipt record and stock movement in a single transaction. Any discrepancy (extra items, missing items) is flagged and requires a supervisor override.
For outbound picking (issuing items for production or customer delivery), the picking flow is: warehouse staff opens a Pick Order (created by sales or production), scans each item as it's picked from the shelf, confirms the bin location by scanning the shelf barcode, and marks items as picked. The system validates the scanned item matches the pick order line — if the wrong item is scanned, an error sound plays and a warning message shows. This scan-to-verify approach prevents pick errors that send the wrong product to a customer.
// React PWA: Barcode scanning with QuaggaJS
import Quagga from 'quagga';
import { openDB } from 'idb'; // IndexedDB for offline storage
function BarcodeScanner({ onScan }: { onScan: (code: string) => void }) {
useEffect(() => {
Quagga.init({
inputStream: {
type: 'LiveStream',
target: document.querySelector('#scanner-container'),
constraints: { facingMode: 'environment' },
},
decoder: { readers: ['code_128_reader', 'ean_reader'] },
}, (err) => {
if (err) { console.error(err); return; }
Quagga.start();
});
Quagga.onDetected((result) => {
const code = result.codeResult.code;
if (code) {
navigator.vibrate?.(100); // haptic feedback
onScan(code);
Quagga.stop();
}
});
return () => Quagga.stop();
}, [onScan]);
return <div id="scanner-container" className="w-full h-64" />;
}
// Offline-first: queue scan when offline, sync when online
const db = await openDB('warehouse-offline', 1, {
upgrade(db) {
db.createObjectStore('pending-scans', { keyPath: 'id', autoIncrement: true });
},
});
async function submitScan(scan: WarehouseScan) {
if (navigator.onLine) {
await fetch('/api/movements', { method: 'POST', body: JSON.stringify(scan) });
} else {
await db.add('pending-scans', { ...scan, timestamp: Date.now() });
}
}
// Sync on reconnect
window.addEventListener('online', async () => {
const pending = await db.getAll('pending-scans');
for (const scan of pending) {
await fetch('/api/movements/batch', {
method: 'POST', body: JSON.stringify(scan)
});
await db.delete('pending-scans', scan.id);
}
});The scanning system consists of: a NestJS WarehouseModule (goods receipt, pick order, and bin management APIs), a React PWA deployed to Cloudflare Pages (for global CDN and zero-config HTTPS), and a Zebra ZT410 label printer for barcode label generation. The NestJS API uses optimistic locking when writing stock movements — if two scanners try to post a movement simultaneously (rare but possible), the second write fails gracefully with a 'please rescan' message. Labels are generated via a NestJS endpoint that returns ZPL code, sent to the printer via the Zebra Link-OS web API.
Warehouses often have poor WiFi coverage in corners, behind racking, and near large metal structures. A scanning app that requires continuous internet connectivity will frustrate warehouse staff when the connection drops mid-receipt. We implemented offline-first scanning: the PWA caches the current day's POs and item master locally using IndexedDB. Scanning works offline — scans queue locally. When connectivity is restored, the queue syncs to the server automatically. Test your WiFi coverage map before go-live and add access points where needed — don't assume good coverage.
For our manufacturing client who handles perishable materials, lot tracking is critical. Every inbound GR captures a lot number and production date. Barcodes encode the lot number (using GS1-128 with Application Identifiers 10 for lot and 17 for expiry date). The inventory system tracks stock by item + lot + location. FEFO (First Expired First Out) picking automatically selects the lot with the earliest expiry date for outbound picks. The ERP alerts the warehouse manager 30 days before any lot expires, providing time to use or return the stock.
Regular cycle counts (instead of annual full stocktake) are more accurate and less disruptive. Our system supports scheduled cycle count tasks: warehouse staff receives a count task on their PWA showing which bins to count. They go to each bin, scan the bin barcode (to confirm location), then scan each item and enter the physical count. The system computes variances on the fly — if a bin shows 50 in the system and the scanner records 47, it highlights the 3-unit discrepancy. The count supervisor reviews all variances before they're posted as ADJ adjustments to the inventory ledger.