Repository navigation
Hardware Setup
No configuration needed. Any USB barcode scanner that acts as a HID keyboard (i.e. "just types" the barcode followed by Enter — true of the vast majority of consumer scanners) works out of the box. The frontend listens for keystrokes and, based on inter-keystroke timing, tells scanner input apart from a human typing so it never interferes with normal form fields.
No USB scanner? Every place a barcode can be typed or scanned — POS checkout and the Add/Edit Product form — also has a Scan with camera button that opens a live preview and decodes barcodes/QR codes directly in the browser. On a phone or tablet it requests the back camera by default, with a Flip camera button to switch. This works alongside the USB scanner, not instead of it — either path feeds the same lookup/add-to-cart logic. The camera requests a 1080p frame and continuous autofocus where the device supports it — a small code held close to the lens needs enough resolution and focus to resolve, not just camera permission.
Browsers only allow camera access on a secure context: an https://
origin, or localhost on the machine itself. The default setup serves the
app over plain http://<lan-ip>:1994, so opening it from a phone/tablet
works for everything except the camera — the browser blocks it
silently, no permission prompt, before the app can even ask. On the till
machine itself this is a non-issue (http://localhost:1994 is always
secure); the gap only shows up on a second device over the LAN.
Three fixes, pick whichever fits:
-
Android / desktop Chrome or Edge — no server change. Visit
chrome://flags/#unsafely-treat-insecure-origin-as-secure, addhttp://<this-machine's-LAN-IP>:1994, enable, restart the browser. Per-browser, per-device; only do this on a network you trust. -
iPhone/iPad, or any browser without that flag (Safari has none) —
bundled HTTPS front door.
Then open
docker compose --profile https up -d
https://<this-machine's-LAN-IP>(no:1994) on the device. Caddy self-signs a certificate automatically; every browser shows a one-time "not trusted" warning to tap past, then the camera works. Same app, same login, same data — just an extra reverse-proxy hop. The plainhttp://<ip>:1994path keeps working unmodified either way. -
Any device, no cert warning, works from outside the LAN too —
Cloudflare Tunnel.
Gets a real, publicly-trusted certificate at the cost of traffic routing through Cloudflare instead of staying fully local.
# Free, no account, URL changes on restart docker compose --profile cloudflare-quick up -d # Stable URL on your own domain — needs CLOUDFLARE_TUNNEL_TOKEN in .env docker compose --profile cloudflare up -d
Not every product arrives with a scannable barcode. The Add Product
form has a Generate button that produces a structurally valid EAN-13
using GS1's 20–29 restricted/internal-use prefix range — checked
against your catalog for uniqueness, retried on collision. From Inventory,
the barcode icon on any row opens a Barcode / QR code label,
rendered client-side (no network call) — Print label or Download
PNG/JPG.
Three ways to get a receipt out:
| Method | How it works | Setup needed |
|---|---|---|
| Opens a formatted receipt, triggers the browser's own print dialog — pick any printer your OS/CUPS knows about | Set up the printer once at the OS level, same as for any app | |
| Download PDF | Server-generates a real PDF (pure-JS pdfkit, no native deps) |
None |
| Print via USB | Sends raw ESC/POS bytes straight to a USB thermal printer, no dialog | One-time Docker device passthrough, see below |
Settings → Receipt → Print automatically after every sale skips the extra click on Print. It does not trigger USB printing (no dialog to skip); use "Print via USB" directly for that, or set Auto-print using → Direct USB.
On Linux this works by default after docker compose up — no vendor/
product ID to configure. Two transports are tried in order:
-
Kernel usblp character device (
/dev/usb/lp0) — the same path a plainecho "hi" > /dev/usb/lp0writes to. This is the primary transport because most cheap 80mm thermal printers present a vendor-specific USB class, which a class-based scan would miss but the kernel's ownusblpdriver already binds. - libusb raw bulk transfer — fallback for printers exposing the standard USB Printer class; detaches the kernel driver first so the interface can be claimed.
Check it works — one click, no sale needed: Settings → Receipt → USB printer check →
- Check printer reports the kernel printer nodes and any USB printer-class device the backend can see.
- Print test slip sends a real cut-off test receipt.
Set USB printer paper width (80mm standard / 58mm compact) to match your roll.
What the container needs (already configured in docker-compose.yml,
Linux host only): the backend service bind-mounts /dev/bus/usb and
grants device_cgroup_rules on two device majors — 189 (usbfs, libusb)
and 180 (usblp, the kernel printer driver). This is not
privileged: true — the container still can't touch anything else on the
host. Nothing fails at compose up when no printer is attached.
Does it need root? Yes, and this is already configured — do not add
anything extra. The kernel printer node is owned root:lp and the
container mknods it on demand; both require root inside the container.
backend/Dockerfile has no USER line and docker-compose.yml sets no
user: for the backend on purpose.
If a printer works from a terminal (echo hi > /dev/usb/lp0) but not from
the app, or from Windows but not Debian — see
Troubleshooting.