There are three main options: dedicated handheld scanners (Zebra, Honeywell — rugged but ~Rp 3–8M each), mobile phones with camera-based scanning (free hardware but slower), and fixed readers at dock doors (Rp 15–30M, ideal for high-volume receiving). For the Commsult client a hybrid approach was used — fixed readers at the receiving dock and mobile phones running a PWA scanning app — to balance cost and performance.
Manufacturer barcodes (GS1) are often not present in the ERP's item master, so scanning them returns no match and wastes time. Printing Code 128 labels that encode your internal SKU using ZPL sent to a Zebra label printer ensures every scan resolves instantly. Each label includes the barcode, item code, item name, and lot number, and the one-time setup takes roughly one day but saves hours of lookup time every week.
The PWA implements an offline-first approach: the current day's purchase orders and item master are cached locally using IndexedDB, so scanning continues to work even when the connection drops. Scans are queued locally and automatically synced to the NestJS backend when connectivity is restored. This prevents mid-receipt failures that would frustrate warehouse staff working in corners or near large metal racking.
When a warehouse worker scans an item during a pick order, the system validates that the scanned barcode matches the specific pick order line. If the wrong item is scanned, an error sound plays and a warning message is displayed immediately. This real-time check stops incorrect products from reaching customers before the mistake can propagate further downstream.
Every inbound goods receipt captures a lot number and production date encoded in a GS1-128 barcode using Application Identifiers 10 (lot) and 17 (expiry date). Inventory is tracked by item, lot, and location, and outbound picks follow FEFO (First Expired First Out) to automatically select the earliest-expiring lot. The ERP also alerts the warehouse manager 30 days before any lot expires, giving time to consume or return the stock.
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. As a freelance ERP consultant, I implemented barcode scanning for a manufacturing client's warehouse — integrating handheld scanners and mobile phone cameras with the custom ERP's inventory module I had built for them. 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 this client, I used a hybrid approach: fixed readers at the receiving dock and mobile phones with a PWA scanning app for warehouse staff. This balanced cost and performance.
I 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), I also added support for 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 freelance ERP consulting work: 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. I print Code 128 barcodes encoding the 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. I 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 this 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. The 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.