Repository navigation
Architecture
Browser / LAN tablet
│ http://<machine>:1994 (the ONLY exposed port)
▼
┌──────────────────────┐ /api/* proxied server-side ┌──────────────────────┐
│ frontend :1994 │ ──────────────────────────────▶ │ backend (internal) │
│ Next.js / React │ ◀────────────────────────────── │ Express + Prisma │
└──────────────────────┘ (internal Docker network) └──────────┬───────────┘
│
▼
SQLite (nodedr-pos_data)
(named Docker volume)
The browser only ever talks to the frontend on port 1994. The Next.js
server proxies every /api/* request to the backend over the internal
Docker network — the backend is not published to the host at all. This
buys three things:
- the app works from any device on the LAN (a counter tablet, a phone), not just the machine running the containers;
- session cookies are first-party, so there's no cross-origin/CORS fragility;
- the API isn't exposed on the network, shrinking the attack surface.
The backend and frontend are two separate containers, neither of which needs
elevated privileges or host device access — everything still comes up with
a single docker compose up. Printing goes through the browser's own print
dialog or a server-generated PDF for most receipts; "Print via USB" is the
one path that talks to hardware, and it does so from the backend with
narrowly scoped device permissions (see Hardware Setup),
not a privileged: true container.
The backend needs the ability to talk to a USB printer; the frontend never does. Splitting them means only the backend carries that (scoped) hardware access, and the frontend stays fully unprivileged.
The .deb and .exe installers don't run Docker at all, but they preserve
the same two-process architecture: systemd units on Linux, Windows
services on Windows, each process supervised and restarted the way the two
containers are here. See Installation for which method fits
your till.
| Layer | Choice |
|---|---|
| Frontend | Next.js (App Router), React, TypeScript, Tailwind CSS |
| Data layer | TanStack Query, react-hook-form + Zod |
| Backend | Node.js, Express, Zod validation, helmet, rate limiting |
| Database | SQLite via Prisma ORM (better-sqlite3 driver adapter), persisted in a Docker volume |
| Auth | bcrypt hashing, HttpOnly JWT cookie, admin/cashier roles |
| API access | Browser → Next.js (:1994) → server-side /api proxy → backend |
| Hardware | Browser print dialog + pdfkit for receipts; a custom React hook for the barcode scanner; @zxing/browser for camera scanning |
| Barcodes |
jsbarcode (EAN-13/CODE128) + qrcode, both rendered client-side to canvas — no external barcode service |
| Charts |
recharts for the dashboard's sales trend, top products, and payment mix graphs |
| Bulk import |
multer (multipart upload) + csv-parse, admin-only, for Reference Data CSV imports |
| Deployment | Docker Compose, two node:24-alpine multi-stage images |
-
GST is backed out of the price, never added on top. In India, MRP
(the price a shop enters) is legally required to be GST-inclusive. The
pricing engine (
backend/src/lib/pricing.js, mirrored infrontend/lib/quote.tsfor the live POS preview) computestaxAmount = inclusiveAmount - inclusiveAmount / (1 + rate/100)and never adds tax to the subtotal — it's already inside it. - Order-level discounts apply to the tax-inclusive subtotal, not a backed-out exclusive base — that's the only ordering where both a percent discount and a flat-rupee discount produce the rupee amount the cashier actually typed.
-
Running money balances (
totalDue,creditBalance,totalSpent) are read-modify-write in JS, rounded, and stored viaset— never a database-sideincrementon a floating-point column, which accumulates binary residue across many small transactions. - Checkout is a single Prisma transaction that server-prices every item, validates stock, and applies discount/loyalty/due/credit atomically.
- All of the above is deliberately duplicated across two "why" write-ups if you go digging in commit history — this page states the current, correct behavior; treat it as the spec if you're extending the pricing or balance code.
For endpoint-level detail see API Reference; for the full project layout see the README's Project structure section.