NodeDR POS Documentation
Everything you need to install, configure, and operate NodeDR POS in your shop. Scroll through — every section is on this one page.
Introduction
NodeDR POS
Free, open-source, offline-first Point of Sale for small retail shops. One Docker install, no subscription, no data ever leaves the store.
What is NodeDR POS?
NodeDR POS is a complete Point of Sale system designed for small retail shops — kiranas, pharmacies, clothing stores, electronics shops, and any counter-based business. It runs as a two-container Docker stack on a local machine and is accessible from any device on your shop’s network. Once installed, it works with no internet connection and makes zero outbound calls — your sales data never leaves the building.
Quick orientation
If this is your first time, follow these three steps:
- Install NodeDR POS — one command, under 10 minutes.
- Set up your hardware — barcode scanner and thermal printer.
- Make your first sale — scan items, select payment, print receipt.
Key capabilities
- Barcode-driven checkout — scan to add, Enter to finalize. Unknown barcodes surface a toast, not a crash.
- GST billing — MRP-inclusive tax with CGST/SGST breakdown, GSTIN on receipt, per-product HSN/SAC codes.
- Three receipt modes — browser print dialog, server-side PDF download, or direct ESC/POS over USB to a thermal printer.
- Loyalty program — points earned per purchase, redeemable at checkout.
- Customer dues (udhaar) — sell on credit, track balances, record repayments.
- Returns & exchanges — look up any past invoice from the register, return items, net against a new sale.
- Multi-currency — 20+ major currencies, switchable in Settings.
- Staff roles — admin and cashier with scoped permissions.
- LAN access — any tablet or phone on the same network can open the register.
Browse the documentation
Getting Started
Using NodeDR POS
Administration
Architecture at a glance
Browser / LAN tablet
│ http://<machine>:1994 ← only exposed port
▼
┌─────────────────┐ /api/* ┌──────────────────┐
│ Next.js :1994 │ ───────▶ │ Express :4000 │
│ (frontend) │ ◀─────── │ + Prisma + SQLite│
└─────────────────┘ internal└──────────────────┘
data → Docker volumeThe frontend is the only container exposed on your network. The backend lives entirely on the internal Docker network — it is not reachable from the LAN. All session cookies are first-party and HttpOnly.
License & attribution
NodeDR POS is released under the GNU Affero General Public License v3.0 (AGPL-3.0). It is free to use, self-host, fork, modify, and redistribute — including for commercial use. If you run a modified version as a network service for others, you must make that modified source available to those users under the same license. Built and maintained by NODEDR INFOTECH PRIVATE LIMITED.
Installation
Get NodeDR POS running on your machine in under 10 minutes. No prerequisites required — the quick-start command below sets up everything, including Docker and git themselves.
Zero-prerequisite quick start (recommended)
Nothing needs to be pre-installed — paste one command in a terminal (macOS/Linux) or PowerShell (Windows) and it installs Docker and git first if either is missing, then clones the repo and starts the stack:
curl -fsSL https://raw.githubusercontent.com/Raktim94/nodedr-pos/master/scripts/quickstart.sh | bashWindows (PowerShell):
irm https://raw.githubusercontent.com/Raktim94/nodedr-pos/master/scripts/quickstart.ps1 | iexSee the homepage quick-start section for per-OS notes. Safe to re-run any time — an existing checkout is updated in place rather than clobbered.
Prerequisites (manual/advanced install only)
If you’d rather install Docker and git yourself and skip the bootstrap script, you need:
- Docker Engine with the Compose v2 plugin (bundled in Docker Desktop for Mac/Windows, and in Docker Engine 24+ on Linux). Run
docker compose versionto check — you need v2.x, not the legacydocker-composecommand. - Git to clone the repository.
- Any modern 64-bit machine: Linux (x86_64 or ARM64), macOS (Intel or Apple Silicon), or Windows with WSL2.
No Node.js, no database setup, no config files — Docker handles everything.
Clone-and-run install
git clone https://github.com/Raktim94/nodedr-pos.git && cd nodedr-pos && ./install.shThe install.sh script does the following automatically:
- Checks that Docker and Compose are installed and healthy.
- Runs
docker compose build— builds both the frontend and backend images from source using multi-stagenode:24-alpinebuilds. First run takes 3–5 minutes; subsequent runs are cached and take under a minute. - Runs
docker compose up -d— starts both containers in the background and creates thenodedr-pos_datanamed volume on first run. - Polls the backend health endpoint until it reports healthy.
- Prints
http://localhost:1994and tells you it’s ready.
Re-run ./install.sh any time to rebuild after pulling updates — it handles everything safely.
Manual install (step by step)
If you prefer to run each step yourself or the script doesn’t fit your environment:
# 1. Clone the repository
git clone https://github.com/Raktim94/nodedr-pos.git
cd nodedr-pos
# 2. Build both images (node:24-alpine multi-stage)
# First run: 3–5 minutes. Subsequent runs: fast (cached).
docker compose build
# 3. Start the stack in the background
# Creates the nodedr-pos_data volume automatically on first run.
docker compose up -d
# 4. (Optional) Watch logs until you see "listening on port 4000"
# and the Next.js ready message
docker compose logs -f
# 5. Open in your browser
# http://localhost:1994First launch — guided onboarding
When you open the app for the first time, a setup wizard runs before you see the dashboard. You will be asked for:
- Admin account — your full name, email address, and a password. This becomes the owner/admin login.
- Shop setup — shop name, address, currency symbol, and a low-stock threshold. You can optionally enter your GSTIN here; GST can be toggled on/off in Settings later.
After completing onboarding you land on the dashboard, ready to add products and start selling.
Changing the port
The default port is 1994. Copy .env.example to .env in the repo root and setHOST_PORT and FRONTEND_ORIGIN to match — docker-compose.yml reads this file automatically, so the compose file itself never needs editing:
# .env
HOST_PORT=2000
FRONTEND_ORIGIN=http://localhost:2000The two values must agree — FRONTEND_ORIGIN is used for CORS so the backend only accepts requests from the correct origin. Then restart the stack: docker compose up -d.
Deploying to a VPS
NodeDR POS runs the same way on a VPS as it does on a shop LAN box — no code changes, and docker-compose.yml is never edited either way. The only difference is three values in .env:
# .env — VPS / publicly reachable host
HOST_PORT=1994
FRONTEND_ORIGIN=https://pos.yourdomain.com
COOKIE_SECURE=truePut a reverse proxy with real HTTPS in front (Caddy or Nginx + Let’s Encrypt), point it at HOST_PORT, then run docker compose up -d. Setting COOKIE_SECURE=true ensures the session cookie is only ever sent over HTTPS — leave it false for local/offline use, where HTTP is expected and fine.
Accessing from other devices on the LAN
Any device on the same network (a counter tablet, a second PC, a phone) can open the register by navigating to:
http://<IP-of-the-machine-running-Docker>:1994Find your machine’s LAN IP with ip addr (Linux) or ipconfig (Windows) or System Preferences → Network (macOS). No per-device install is needed — the browser is the client.
Stopping and restarting
# Stop containers (data is preserved in the volume)
docker compose down
# Start again
docker compose up -d
# Stop AND delete all data (use only to start fresh)
docker compose down -vnodedr-pos_data Docker volume, not inside the containers. Stopping, removing, or rebuilding containers never touches your data. Only docker compose down -v or an explicit docker volume rm nodedr-pos_data deletes it.Updating to a new version
cd nodedr-pos
git pull
docker compose up -d --buildOr re-run ./install.sh — it does exactly the same thing. Your data in the volume is untouched.
CasaOS / ZimaOS: one-click app store install
Already running a CasaOS or ZimaOS home server? Install NodeDR POS straight from a compose URL — no terminal, no git clone, no build step. CasaOS pulls the pre-built images directly from GHCR.
- In CasaOS/ZimaOS, go to App Store → + (top right) → Install a customized app (or the equivalent Custom Install option on ZimaOS).
- Paste this compose URL:
https://raw.githubusercontent.com/Raktim94/nodedr-pos/master/casaos/docker-compose.yml - Confirm the install-time configuration CasaOS shows you — web UI port
1994, the data directory, and the optional USB printer passthrough — then click Install. - Open it from the CasaOS dashboard, or go straight to
http://<your-casaos-box>:1994.
Data persists at /DATA/AppData/nodedr-pos/data on the CasaOS box, following the same backup/restore convention CasaOS uses for every other app. Images are built for both amd64 and arm64, so this works on x86 mini-PCs and ARM SBCs alike.
Windows 10/11: native installer (no Docker)
If you would rather not install Docker at all, NodeDR POS also ships as a native Windows installer. It bundles its own Node.js runtime, installs the app under Program Files\NodeDRPOS, and registers two Windows services — NodeDR POS Backend and NodeDR POS Web Interface — that start automatically at boot, with no one logged in.
Download the latest installer and run it (requires admin). It is not code-signed yet, so Windows SmartScreen shows “Windows protected your PC” on first run — click More info → Run anyway. See all versions on the GitHub Releases page.
Managing the services
nodedr-pos open :: open the POS (starts the services if stopped)
nodedr-pos doctor :: check services, port and database
nodedr-pos restart :: needs admin
nodedr-pos logs :: open the log folderAlso available from the Start Menu. Windows Firewall is opened for port 1994 only — the internal API port stays blocked.
C:\ProgramData\NodeDRPOS, outside the installed application files. Installing a newer version over an older one preserves it, and a silent uninstall never deletes it — the uninstaller asks separately. Take a backup first with nodedr-pos backup.Debian / Ubuntu: native package (no Docker)
If you would rather not install Docker at all, NodeDR POS also ships as a native .deb package for Debian and Ubuntu. It bundles its own Node.js runtime, installs the app under /opt/nodedr-pos, and runs as a systemd service that starts automatically on boot — nothing to type in a terminal after the download.
sudo apt install ./nodedr-pos_1.0.0_amd64.deb
# or double-click the file in a desktop file managerThe installer creates a dedicated nodedr-pos system user, sets up the SQLite database, enables the service, and opens http://localhost:1994 once it is ready — the same first-run onboarding wizard as the Docker install.
Download the latest .deb or see all versions on the GitHub Releases page.
Managing the service
sudo systemctl status nodedr-pos # is it running?
sudo systemctl restart nodedr-pos # restart after a config change
nodedr-pos doctor # check ports, database, printer wiring
nodedr-pos logs # follow the logsConfiguration
Settings such as the port and bind address live in /etc/nodedr-pos/nodedr-pos.conf. Edit it and run sudo systemctl restart nodedr-pos to apply changes. The file survives package upgrades.
/var/lib/nodedr-pos, outside the installed application files. sudo apt remove nodedr-pos keeps your data — only sudo apt purge nodedr-pos deletes it. Take a backup first with sudo nodedr-pos backup.Prefer Docker, want to run on macOS, or need multi-machine deployment (or a VPS)? Use the Docker install above instead — every method runs the identical application.
Windows: native installer (no Docker)
NodeDR POS also ships as a standard Windows installer. It bundles its own Node.js runtime, installs to C:\Program Files\NodeDRPOS, and sets the app up as two Windows Services that start automatically at boot — nothing to type in a terminal, nobody needs to be logged in for the till to be running.
Download the latest installer (.exe) or see all versions on the GitHub Releases page. Targets 64-bit Windows 10 and 11 only.
Operator commands
Available from the Start Menu, or a terminal:
nodedr-pos open :: open the POS (starts the services if stopped)
nodedr-pos doctor :: check services, port and database
nodedr-pos status
nodedr-pos restart :: needs admin
nodedr-pos backup :: online, crash-safe copy of the database (admin)
nodedr-pos logs :: open the log folderC:\ProgramData\NodeDRPOS, outside the installed application files. Uninstalling keeps your data by default — the uninstaller asks separately before deleting it, and a silent uninstall never does. Take a backup first with nodedr-pos backup.Hardware Setup
NodeDR POS works with standard off-the-shelf retail hardware. No proprietary devices, no drivers to install in most cases.
Barcode scanner
Any USB barcode scanner that acts as a HID keyboard works plug-and-play. This includes the vast majority of consumer and commercial scanners — when you scan a barcode, the device “types” the code into the focused field and then sends an Enter keystroke. No driver installation, no configuration.
NodeDR POS uses a custom useBarcodeScanner hook in the frontend that distinguishes scanner input from human typing by measuring inter-keystroke timing (scanners type much faster than humans). This means:
- The scanner works on any page — POS, Inventory, or the product search — without you needing to click a specific input first.
- Scanning never interferes with text you’re typing manually in a form field.
- A known barcode adds the product to the cart (or increments quantity if already there).
- An unknown barcode shows a brief toast notification and does not block the register.
Recommended scanner types
- 1D USB HID scanners (EAN-13, CODE128, CODE39) — the most common counter scanner. Plug in and go.
- 2D / QR scanners — work equally well; NodeDR POS stores QR codes in the barcode field the same as 1D codes.
- Wireless (2.4GHz USB dongle) — the dongle appears as a USB HID device to the OS, so it works identically.
- Bluetooth scanners — work as long as they are paired to the machine running Docker (they appear as an HID keyboard).
Camera barcode & QR scanning
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 camera preview and decodes barcodes and 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 above, not instead of it — either input path feeds the exact same lookup/add-to-cart logic. The camera requests a 1080p frame and continuous autofocus where the device supports it, aimed at the same problem: a small code held close to the lens needs enough resolution and focus to resolve.
Camera scanning over HTTPS
Browsers only allow camera access on a secure context: an https:// origin, or localhost on the machine itself. The default Docker setup serves the app over plain http://<lan-ip>:1994, so opening it from a second device works fine for everything except the camera — the browser blocks it silently before the app ever gets a chance to 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 phone/tablet over the LAN. Three ways to fix it:
- Android / desktop Chrome or Edge — no server change needed. Visit
chrome://flags/#unsafely-treat-insecure-origin-as-secure, addhttp://<this-machine’s-LAN-IP>:1994, enable the flag, and restart the browser. This is a per-browser setting — repeat on each device. - iPhone/iPad, or any browser without that flag — turn on the bundled HTTPS front door. A
caddyservice ships indocker-compose.ymlbehind thehttpsCompose profile, off by default:Then opendocker compose --profile https up -dhttps://<this-machine’s-LAN-IP>(no:1994) on the phone/tablet. Caddy generates a self-signed certificate automatically — every browser, including Safari on iOS, shows a one-time “this certificate isn’t trusted” warning to tap past, then the camera works from then on. - Any device, no cert warning, works from outside your LAN too — Cloudflare Tunnel. Gets you a real, publicly-trusted certificate at the cost of routing traffic through Cloudflare’s network. Two modes, both off by default:
# Free, no account needed — URL changes on restart docker compose --profile cloudflare-quick up -d # Stable URL on your own domain — needs a Cloudflare # account + tunnel token (CLOUDFLARE_TUNNEL_TOKEN in .env) docker compose --profile cloudflare up -d
Barcode & QR label generator
Not every product arrives with a printable barcode — loose produce, house brands, or anything repackaged in-store. NodeDR POS has an offline barcode generator built in so you never need an external service.
- In the Add Product form, click Generate next to the barcode field. The system produces a structurally valid EAN-13 using the
20–29prefix range that GS1 reserves for restricted/internal circulation — these are never real resellable product codes. - The code is checked against your existing catalog for uniqueness and retried automatically if there is a collision.
- From the Inventory page, the barcode icon on any row opens a label modal with a Barcode / QR code toggle. The label is rendered client-side with
jsbarcode/qrcode— no network call, fully offline. - Print label opens a new tab and calls
window.print()— any printer the OS knows about. - Download PNG or JPG saves the label as an image for use in Word, Canva, or any label template.
Receipt printing — three modes
After checkout (and from the Sales history), three print buttons are available:
1. Print (browser print dialog)
Opens a formatted receipt in a hidden iframe and immediately calls window.print(). Your OS print dialog appears, showing every printer configured — a USB thermal printer set up via CUPS (Linux/macOS), Windows print spooler, a network printer, or “Save as PDF”. Set up the thermal printer once at the OS level and it shows here automatically.
The backend is not involved in this path at all — it’s a pure browser action.
2. Download PDF
Generates a real PDF server-side using pdfkit (pure JS, no native dependencies, no shell-out) and downloads it. Use this for emailing a receipt, archiving, or printing later from any device. PDF page height is computed per-receipt — a one-item receipt is a short page, not a blank A4.
3. Print via USB (ESC/POS)
Sends raw ESC/POS commands directly to a USB thermal printer from the backend. No print dialog appears — the receipt cuts off the roll immediately after you click.
This mode requires two one-time steps:
Step 1 — Docker USB passthrough
The docker-compose.yml backend service already includes the necessary device passthrough:
# Already in docker-compose.yml — no changes needed unless your setup is unusual
volumes:
- /dev/bus/usb:/dev/bus/usb
device_cgroup_rules:
- "c 189:* rmw" # USB device major 189 onlyThis is not privileged: true — the backend can only access USB device nodes, nothing else on the host. If you plug in the printer after docker compose up, no restart is needed.
Step 2 — Set paper width in settings
Go to Settings → Receipt → USB printer paper width and select either 80mm (standard) or 58mm (compact). This controls how wide the fixed-column text layout is.
?. Use the Print or Download PDF buttons when exact Unicode text matters.Auto-print setting
Enable Settings → Receipt → Print automatically after every sale to skip clicking Print after each checkout. This only applies to the browser print dialog — it does not trigger USB printing (which has no preview step to skip).
Supported printer compatibility
For the browser print and PDF modes, any printer that the OS recognizes works. For the direct USB mode:
- Any USB thermal printer that advertises the standard USB Printer device class (
bDeviceClass 7) works without configuration. - Tested with generic 80mm and 58mm ESC/POS thermal receipt printers from common brands.
- No vendor/product ID to configure — the backend scans for the first connected USB printer class device.
- Runs on Linux hosts only for the USB path (Docker’s
/dev/bus/usbpassthrough is a Linux feature). macOS and Windows users should use CUPS / Windows print spooler with the browser print button instead.
POS Checkout
The POS screen is built for speed — scan barcodes to fill the cart, choose a payment method, and print the receipt, all without touching a mouse.
Opening the register
Click POS in the sidebar. The register opens with an empty cart. If the machine has a barcode scanner, it is active immediately — no need to click an input first.
Adding items to the cart
By barcode scan
Scan the product barcode. NodeDR POS distinguishes scanner input from human typing by inter-keystroke timing (scanners send digits in rapid succession; humans do not). If the barcode is in the catalog:
- The item is added to the cart with qty 1.
- Scanning the same barcode again increments the quantity.
If the barcode is not in the catalog, a toast notification appears (“Barcode not found”) and the register stays open — the cashier can continue with other items.
By name/search
Type the product name in the search box at the top of the POS screen and select from the dropdown. Useful for products without barcodes or when the scanner is unavailable.
Adjusting quantity
Click the + / − buttons on a cart line, or type directly into the qty field. Removing all quantity from a line removes it from the cart.
Attaching a customer
Type a phone number in the Customer field at the top of the cart and press Enter or click Search. If the customer is found:
- Their loyalty point balance is shown.
- Any outstanding due (udhaar) is shown as a warning so the cashier is aware before completing the sale.
- Their name appears on the printed receipt.
Attaching a customer is optional for regular sales, but required for using loyalty points or creating a due (partial payment).
Applying a discount
Two discount mechanisms are available:
- Per-product standing discount — set once in Inventory as a percentage; applied automatically every time that product is sold. Shown on the cart line as a strikethrough price.
- Per-sale discount — enter a percentage or flat-amount discount in the Discount field at the bottom of the cart. Applied correctly across mixed tax rates before tax breakdown is computed.
Redeeming loyalty points
If a customer with points is attached, a Redeem points field appears. Enter the amount to redeem (in currency, not points) or click Use all to apply the full balance. The points are converted at the rate configured in Settings (e.g. 100 points = ₹10 / $0.10). Redeemable points are capped at the customer’s actual balance — you cannot overdraw.
Payment methods
Select the payment method before finalizing:
- Cash — enter the amount tendered; the system calculates and displays change. If the amount entered is less than the total and a customer is attached, the shortfall is recorded as a due (see Customer Dues).
- UPI — tap to finalize. No integration with a payment gateway; this simply records the method. The cashier verifies the UPI payment on their own device.
- Card — same as UPI: records the method without gateway integration.
Finalizing the sale
Press Enter (when the cart is focused) or click the Checkout button. The server:
- Re-computes the price, tax, discount, and loyalty redemption from the catalog and current settings — client values are never trusted for money.
- Decrements stock for each sold item.
- Creates the invoice, invoice lines, and any due/loyalty transactions.
- Returns the invoice ID and receipt data.
The cart is then cleared and three receipt buttons appear: Print, Download PDF, and Print via USB.
Receipt layout
================================================
Your Shop Name
1 High Street, London
GSTIN: 27ABCDE1234F1Z5
================================================
Date: 25-07-2026 14:32 Bill: #INV-2026-00047
Cust: Alex Johnson
Ph: +44 7700 900000
------------------------------------------------
Item Qty Rate Amount
------------------------------------------------
Mineral Water 2 1.50 3.00
Premium Coffee 1 4.50 4.50
Discount (10%) -0.75
------------------------------------------------
Subtotal £6.75
Loyalty (50 pts) -£0.50
================================================
GRAND TOTAL £6.25
================================================
Paid (Card) £6.25
------------------------------------------------
You earned 6 loyalty points!
================================================
Thank you! Visit again.
================================================The header, footer, GSTIN display, currency symbol, and loyalty summary are all driven by your Settings — the same layout adapts to any currency and any shop configuration.
Sales history
Every completed sale is saved in Sales → History. You can:
- Search by date range, customer name, or invoice number.
- Click any row to see the full invoice detail.
- Reprint or re-download the receipt from any past invoice.
- Start a return directly from the invoice detail page.
Inventory Management
Add products, manage stock, generate barcodes, and track low-stock alerts — all from the Inventory page.
Adding products
Go to Inventory → Add Product. Each product has the following fields:
- Name (required) — the product name that appears on receipts and the POS search.
- Barcode — the EAN-13, CODE128, or any other barcode. Scan directly into this field with your scanner, type it, or click Generate to create an internal EAN-13 (see Barcode Generator below).
- Price / MRP (required) — the selling price. This is treated as the final price the customer pays (GST-inclusive if GST is enabled). GST is never added on top; it is only broken out on the receipt.
- GST Rate — select from 0%, 5%, 12%, 18%, 28%, or type a custom rate. Quick-select chips for common rates are shown. Optional — leave at 0% for non-GST items.
- HSN / SAC code — the Harmonised System / Services Accounting Code for the product. If you have imported the HSN/SAC reference data (Settings → Reference Data), this field autocompletes.
- Category — free-text with datalist suggestions from your existing catalog. Groups products in search and filtering.
- Unit (UQC) — standard Unit Quantity Code (PCS, KGS, LTR, BOX, etc.). Shown on receipts and snaphotted at sale time so it never changes retroactively.
- Stock quantity — current stock count. Decremented automatically on every sale.
- Discount % — a standing per-product discount percentage applied automatically every time this item is sold. Distinct from the per-sale discount on the POS screen.
- Description — optional internal notes; not shown on receipts.
Scanning to edit stock
On the Inventory page, scan a barcode to instantly jump to that product’s edit form. If the barcode is not in the catalog, the Add Product form opens with the barcode field pre-filled — the fastest workflow for onboarding new products at the counter.
Editing and deleting products
Click any row in the inventory list to edit. Changes to name, price, GST rate, and unit are not retroactive — existing invoices snapshot the values at the time of sale and are unaffected. Deleting a product is permanent; invoices that referenced it retain their snaphotted data.
Barcode generator
Products that arrive without a barcode (loose goods, house brands, repackaged items) can be assigned an internal code in one click:
- Open the Add Product form.
- Click Generate next to the Barcode field.
- A valid EAN-13 is produced using the
20–29GS1 internal-use prefix range. It is checked against your catalog for uniqueness and retried automatically if there is a collision. - Print a barcode or QR label for the product from the Inventory list (barcode icon on the row).
Barcode and QR labels
Click the barcode icon on any Inventory row to open the label modal. You can:
- Toggle between Barcode (EAN-13/CODE128) and QR code views.
- Click Print label to open a print-ready tab and trigger
window.print(). - Click Download PNG or Download JPG to save the label as an image.
All rendering is done client-side with jsbarcode / qrcode — no network call, works fully offline.
Low-stock alerts
The Sales Dashboard shows a low-stock alert list for any product whose quantity is at or below the low-stock threshold you set during onboarding (editable in Settings). Products with zero stock are highlighted separately.
Allow negative stock
Under Settings → Inventory → Allow negative stock, you can enable selling products whose stock count would go below zero. Useful when your stock counts lag reality (e.g. physical counts happen weekly) and you do not want the register to block sales. When enabled, the product’s stock is simply decremented past zero and shown in red on the Inventory page.
When disabled (the default), attempting to sell more than available stock shows an error at checkout.
Bulk product import
Admin users can import products from a CSV file via Settings → Reference Data. The expected columns are:
name, barcode, price, gstRate, hsnCode, category, unit, stock, discountPercentOnly name and price are required. The import replaces matching barcodes and inserts new rows — it is safe to re-import an updated file.
Tax & Billing
NodeDR POS supports any tax model — GST/VAT/sales tax. Configure per-product rates, choose your currency, and receipts adapt automatically.
How tax works
NodeDR POS uses an inclusive tax model: the price you enter for a product is the final price the customer pays. Tax is never added on top — it is only broken out on the receipt for compliance. This is the correct model for most retail environments globally (GST-inclusive pricing in India, VAT-inclusive pricing in the EU, etc.).
The receipt shows the tax breakdown as an informational line (e.g. “VAT included: £X.XX” or “CGST/SGST: ₹X”). The grand total is always the price the customer was shown on the shelf.
Enabling or disabling tax
Go to Settings → Tax and toggle Enable GST / Tax. When disabled, no tax breakdown appears on receipts and all products are treated as zero-rate. This is useful for regions that have no consumption tax or for businesses below the registration threshold.
Per-product tax rates
Each product in Inventory has its own tax rate. Quick-select chips show common rates (0%, 5%, 12%, 18%, 28%) — click one or type a custom percentage. You can have a zero-rate product (medicines, basic food) sitting alongside an 18%-rate product (electronics) in the same sale; the receipt correctly breaks out each rate separately.
India — GST specifics
When the tax label is set to “GST” in Settings:
- Tax is split equally into CGST (Central) and SGST (State) on receipts, as required by Indian law for intra-state sales.
- Your GSTIN is printed on every receipt once entered in Settings.
- Per-product HSN / SAC codes can be added and print on the receipt line item for B2B compliance.
- Live GSTIN format validation is available in Settings — it checks the structural format and decodes the embedded state code (it does not do checksum validation, so a real but wrong GSTIN is never hard-rejected by the client).
- Live PAN format validation is available in Settings.
Other countries — VAT / sales tax
NodeDR POS is not India-only. To use it with a different tax regime:
- In Settings, change the tax label from “GST” to “VAT”, “Sales Tax”, or whatever applies in your jurisdiction.
- Set per-product rates matching your local rates (e.g. 20% UK VAT, 19% German MwSt, 10% Australian GST).
- The receipt will show your label and the computed amount. The CGST/SGST split is India-specific and is only shown when the tax label contains “GST”.
- Change the currency in Settings to match your country — 20+ currencies are supported.
Currency
Go to Settings → General → Currency to select your currency. Over 20 major currencies are supported including USD ($), EUR (€), GBP (£), INR (₹), AED (د.إ), SAR (﷼), AUD ($), CAD ($), SGD ($), JPY (¥), BRL (R$), ZAR (R), and more. The symbol flows through the entire app and onto all receipts automatically.
HSN / SAC reference data
Full HSN/SAC code catalogs are large (tens of thousands of entries) and updated periodically by tax authorities. Rather than bundle a snapshot that could go stale, NodeDR POS lets you import the current official file yourself:
- Download the current HSN/SAC CSV from the official tax authority in your country (e.g. CBIC in India).
- Go to Settings → Reference Data → Import Tax Codes (admin only).
- Upload the CSV. Expected columns:
code, description, gstRate(rate is optional). - After import, the HSN/SAC field on Add Product autocompletes against the loaded codes.
Each import replaces the existing rows — re-importing an updated file is always safe.
PIN code and IFSC reference data
For Indian businesses, Settings → Reference Data also supports:
- PIN codes — import the India Post dataset. After import, entering a PIN in company settings autofills the city and state.
- IFSC codes — import the RBI IFSC dataset. A standalone IFSC search box is available for looking up a bank branch by code.
These imports are entirely optional — the app works fully without them.
Loyalty Program & Customer Dues
Reward repeat customers with loyalty points and track credit sales with a built-in dues ledger.
Customer directory
Customers are identified by phone number and stored in the Customers page. Each customer record shows:
- Total visit count and lifetime spend.
- Current loyalty point balance.
- Outstanding due balance (if any).
- Full purchase history.
At the POS, type a phone number in the Customer field and press Enter to look up an existing customer or create a new one.
Loyalty program
Earning points
When a sale is completed with a customer attached, the customer earns points based on the amount paid. The earn rate is configurable in Settings → Loyalty:
- Earn rate — e.g. “1 point per $1 spent” or “10 points per ₹100”.
- Points are computed on the final amount paid after discounts and loyalty redemptions.
- The receipt shows how many points were earned at the bottom.
Redeeming points
When a customer with a positive balance is attached at checkout, a Redeem points field appears. Enter the currency amount to redeem (the system converts to points automatically) or click Use all.
- Redemption is capped at the customer’s actual balance — you cannot overdraw.
- The point value is configurable in Settings (e.g. 100 points = $1.00).
- After redemption, points are deducted and the receipt reflects the loyalty discount.
Dashboard ranking
The Sales Dashboard shows a Top Loyalty Customers table ranked by current point balance, so you can identify your most loyal customers at a glance.
Customer dues (credit sales)
Udhaar (credit sales / running tab) lets a customer take goods now and pay later. This is common in small retail worldwide — the corner shop extending credit to a regular.
Creating a due
- Attach a customer (phone number required) at the POS.
- At checkout, select Cash as the payment method.
- Enter an Amount received less than the sale total.
- The system warns before finalizing that the shortfall will be added to the customer’s due.
- Confirm. The sale is completed and the unpaid balance is added to the customer’s running due.
Due warning at POS
If a customer already has an outstanding due, a warning appears when they are attached at the POS — the cashier sees the current balance before ringing up the new sale. A second warning appears if the current sale would add to it.
Recording a payment
- Go to Customers and click the customer.
- Click the due amount to open the payment form.
- Enter the amount received (capped at current balance).
- The payment is recorded in the customer’s due payment history (
CustomerDuePaymenttable) — not just a number decrement — so there is a full audit trail of who paid what and when.
Store credit (from returns)
When a customer returns items, the refund can optionally be issued as store credit rather than cash or UPI. Store credit is a separate creditBalance on the customer record (distinct from the due balance) and can be spent at any future checkout — it appears as a “Use store credit” option at the POS when the customer is attached.
Returns & Exchanges
Handle returns and exchanges from any past invoice at the POS screen — no separate returns page, no paper forms.
How returns work
Returns are initiated directly from the POS screen via the Return / Exchange panel. Enter an invoice number to pull up the original sale, then choose which items — and how many — to return. You can combine a return with a new sale in the same checkout to process an exchange as a single transaction.
Processing a standalone return
- Open the POS screen.
- Click Return / Exchange (or press the keyboard shortcut).
- Enter the invoice number (e.g.
INV-2026-00047) and press Enter. - The original sale lines appear. Select the items and quantities to return.
- The return amount is pre-filled as the amount originally paid for those items (after any line discounts). The cashier can lower the refund but not raise it above what the customer paid.
- Choose the refund method: Cash, UPI, Card, or Store Credit.
- Finalize. Returned stock is immediately added back to inventory. The invoice’s return history is updated.
Processing an exchange
An exchange is a return and a new sale in the same checkout:
- Follow steps 1–5 above to queue the return lines.
- Without finalizing, add new items to the cart as you would for a regular sale.
- The return value is netted against the new sale total:
- If the new items cost more than the return value, the customer pays the difference.
- If the return value is more than the new items, the leftover is applied as: first, paying down any outstanding customer due; then, refunded as cash/UPI/card or added as store credit.
- Finalize in one transaction. The return is recorded with
refundMethod: BILL_OFFSETfor the netted portion — no money changes hands for that amount.
Partial and repeat returns
Returning part of a line on one day and the rest on another day is fully supported. The system tracks how much of each line has already been returned by summing all existing ReturnItem rows against the original invoice line — not a running counter. This means:
- You can return 1 of 3 units today and 2 next week — both work correctly.
- A unit that has already been returned can never be returned again — the returnable quantity is always exact.
- The cashier sees the remaining returnable quantity clearly on the return panel.
Stock and accounting behaviour
- Stock is restored to inventory immediately when a return is finalized.
- Loyalty points earned on the original sale are deducted proportionally on a return.
- The original invoice is not modified — returns are stored as separate records linked to the invoice, preserving the full audit trail.
- The sales dashboard excludes returned amounts from revenue totals.
Viewing return history
From Sales → History, click any invoice to see its detail view. A “Returns” section at the bottom lists every return made against that invoice — date, items, quantities, refund method, and amount — so the cashier or admin can see the complete lifecycle of any sale.
Receipts & Printing
Three print modes, one receipt layout. Print to any OS printer, download a real PDF, or cut directly to a USB thermal printer with no dialog.
The three print modes
After every sale, and from the Sales history for any past invoice, three buttons are available:
Print — browser dialog
Opens a formatted receipt in a hidden iframe and immediately triggers window.print(). Your operating system’s print dialog appears, listing every configured printer — a USB or network thermal printer (set up via CUPS on Linux/macOS or Windows print spooler), a regular office printer, or “Save as PDF.” Set up the thermal printer once at the OS level and it shows here automatically.
- Works on all platforms (Linux, macOS, Windows, any OS that runs Docker).
- Full Unicode — renders any language, any currency symbol.
- The backend is not involved in this path at all.
Download PDF
Generates a PDF server-side using pdfkit (pure-JS renderer, no shell-out, no native dependencies) and downloads it to your device. Page height is computed per-receipt — a one-item receipt is a short page, not a blank A4.
- Use this for emailing receipts, archiving, or printing from a device that is not at the counter.
- Full Unicode — correct rendering for all languages and currency symbols.
- Works even on machines with no printer attached at all.
Print via USB — ESC/POS direct
Sends raw ESC/POS commands from the backend directly to a USB thermal printer. No print dialog, no OS driver setup — the receipt cuts off the roll immediately after you click.
- Requires a Linux host (uses Docker
/dev/bus/usbpassthrough — this is a Linux-only feature). - Works with any standard USB ESC/POS thermal printer that advertises USB Printer device class. No vendor/product ID to configure.
- Does not require CUPS or any printer driver.
- One-time Docker setup:
/dev/bus/usbis already bind-mounted and cgroup rules are set indocker-compose.yml.
?. Use Print or Download PDF when exact Unicode is needed.Auto-print setting
Enable Settings → Receipt → Print automatically after every sale to skip the receipt modal entirely and trigger the browser print dialog immediately on checkout. Does not apply to USB printing.
Receipt layout
The receipt is driven entirely by your Settings — no hardcoded content. The layout includes:
- Header — shop name, address, phone, and your custom header text from Settings.
- GSTIN / tax registration — shown only if entered in Settings.
- Invoice number and date — auto-incrementing invoice number in
INV-YYYY-NNNNNformat. - Customer name and phone — if a customer was attached.
- Line items — product name, quantity, unit, rate, and line total. Per-product discount is shown as a strikethrough.
- Tax breakdown — subtotal, tax (CGST/SGST or VAT/GST depending on Settings), loyalty discount, due balance impact.
- Grand total and payment — total amount, payment method, change (for cash).
- Loyalty summary — points earned on this purchase.
- Footer — your custom footer text from Settings (e.g. “Thank you! Visit again.”).
Receipt layout is paper-efficient
Every millimetre of thermal paper costs money over thousands of receipts. NodeDR POS’s HTML receipt CSS has no unnecessary margins, and the PDF page height is computed per-receipt — a two-item sale generates a shorter page than a ten-item sale. There is no fixed A4 or fixed thermal length.
Paper width (USB printing)
Go to Settings → Receipt → USB printer paper width and select 80mm (standard) or 58mm (compact). This controls the column count of the fixed-width text layout sent to the printer. Use 80mm unless your printer’s roll is 58mm wide.
Sales Dashboard
Your shop's performance at a glance — revenue trends, best sellers, payment mix, and today's totals. No cloud, no subscription analytics.
Dashboard sections
Today’s summary
At the top of the dashboard, four stat cards show:
- Today’s revenue — total sales finalized today.
- Transactions — number of completed sales today.
- Average order value — revenue ÷ transaction count.
- Outstanding dues — total unpaid balance across all customers.
Revenue trend chart
A line chart showing daily revenue for the past 30 days (or your selected date range). Built with recharts — rendered entirely in the browser, no external chart service. Hover a point to see the exact amount and date.
Top-selling products
A bar chart showing your best-selling products by revenue over the selected period. Useful for purchasing decisions and identifying products to promote.
Payment method mix
A donut chart showing the split between Cash, UPI, and Card payments. Helps you understand how customers prefer to pay and plan accordingly.
Low-stock alerts
A list of products at or below your configured low-stock threshold. Click any product to go directly to its inventory edit form. Products at zero stock are highlighted separately.
Top loyalty customers
A table of your top customers ranked by current loyalty point balance. Shows customer name, phone, total visits, total spend, and current points.
Date range filtering
All charts and summaries respond to a date range selector at the top of the dashboard. Select a preset (Today, Last 7 days, Last 30 days, This month) or enter a custom range. The charts and stat cards update immediately — all data is computed from your local SQLite database, no server round-trip to a cloud.
CSV export
Click Export CSV on the Sales History page to download your full sales history as a spreadsheet. You can optionally filter by date range before exporting. The CSV includes:
- Invoice number, date, and time.
- Customer name and phone (if attached).
- Each line item: product name, barcode, quantity, unit price, discount, line total, tax rate, CGST, SGST.
- Sale totals: subtotal, total tax, discount, loyalty redeemed, grand total.
- Payment method and amount paid.
Use the CSV for accounting, tax filings, or importing into a spreadsheet tool for custom analysis.
Sales history search
Sales → History is a searchable, paginated list of every completed sale. Search by:
- Invoice number (exact or partial).
- Customer name or phone number.
- Date range.
- Payment method.
Click any row to see the full invoice detail, reprint the receipt, or initiate a return.
Staff Accounts & Roles
NodeDR POS has two roles: admin and cashier. Admins control everything; cashiers run the register.
Roles
Admin
The first account created during onboarding is always an admin. Admins can:
- Access all POS, Inventory, Sales, and Customer features.
- Manage Settings (shop config, tax, loyalty, receipt layout, reference data).
- Create, edit, enable, and disable staff accounts.
- Export sales data to CSV.
- Import reference data CSVs (HSN/SAC, PIN codes, IFSC).
- Access all API routes.
Cashier
Cashier accounts are created by admins. Cashiers can:
- Use the POS register — add items, checkout, print receipts.
- View and search Inventory (read-only; cannot add, edit, or delete products).
- View Sales history and reprint receipts.
- View the Customer directory and record due payments.
- Change their own password.
Cashiers cannot: access Settings, manage other staff accounts, export data, or import reference data.
Creating a staff account
- Log in as admin.
- Go to Settings → Staff.
- Click Add staff member.
- Enter the name, email, and initial password.
- Select role: Cashier (or Admin if you want to grant full access).
- Save. The staff member can now log in at
http://<machine>:1994/login.
Disabling an account
In Settings → Staff, toggle the Active switch off for a staff member. Disabling an account immediately invalidates their session — they are logged out on the next request. The last active admin account cannot be disabled (you would be locked out).
Password management
- Passwords are hashed with bcrypt (cost factor 12). Plaintext is never stored.
- Any user can change their own password from the account menu.
- Admins can reset a staff member’s password from Settings → Staff → Edit.
- Login is rate-limited to 10 attempts per 15 minutes per IP to prevent brute force.
- Login returns an identical error message for unknown email vs. wrong password — no user enumeration.
Session management
Sessions use HttpOnly, SameSite=Lax JWT cookies signed with a secret auto-generated on first boot. Every authenticated request re-checks that the account exists and is active in the database — disabling a staff member logs them out immediately on their next action, not just on next login.
Set COOKIE_SECURE=true in the backend environment when serving over HTTPS (e.g. if you put a reverse proxy in front).
Multiple admins
You can have multiple admin accounts. NodeDR POS prevents the last active admin from being demoted to cashier or disabled — this guard ensures you can never be locked out of your own system.
Settings
All settings are managed by admin users from the Settings page. Changes take effect immediately with no restart required.
General
- Shop name — appears on every receipt header and the browser tab.
- Address — street address, city, and state printed on receipts.
- Phone — shop phone number printed on receipts.
- Currency — select from 20+ supported currencies. The symbol flows through the entire app and all receipts. Changing currency does not affect historical invoice amounts — they are stored as plain numbers.
- Low-stock threshold — products at or below this quantity appear in the dashboard low-stock alert list.
Tax
- Enable tax — toggle tax calculation on or off globally. When off, all products are treated as zero-rate and no tax breakdown appears on receipts.
- Tax label — the label printed on receipts. Set to “GST” for India (triggers CGST/SGST split), “VAT” for EU/UK, “Sales Tax” for the US, or any custom label.
- GSTIN — your tax registration number. Printed on every receipt when entered. Live format validation is provided (structural check, not checksum).
- PAN — your business PAN (India). Stored in settings, available for printing if needed. Live format validation provided.
Loyalty
- Enable loyalty — toggle the loyalty points program on or off.
- Earn rate — how many points a customer earns per currency unit spent. E.g. “1” means 1 point per $1.
- Point value — how much one point is worth in currency. E.g. “0.01” means 100 points = $1.00.
- Min redemption — minimum points required before a customer can redeem at checkout.
Receipt
- Header text — custom text printed above the shop name on receipts (e.g. a tagline or branch name).
- Footer text — custom text printed at the bottom of every receipt (e.g. “Thank you! Visit again.”).
- Show tax breakdown — whether to show CGST/SGST (or VAT) lines on the receipt. Some businesses prefer a single “Total tax” line; others need the full split.
- Auto-print after sale — immediately trigger
window.print()on checkout without a confirmation modal. - USB printer paper width — 80mm or 58mm. Controls the column count for direct ESC/POS printing.
Inventory
- Allow negative stock — when enabled, products can be sold even if their stock count would go below zero. Stock is shown in red when negative. Useful when physical counts lag behind system counts.
Staff
Manage staff accounts, reset passwords, and toggle active status. See Staff Accounts & Roles for details.
Reference Data (admin only)
Import large, frequently-updated government datasets for autocomplete and lookup features:
- HSN / SAC codes — tax codes for product classification. After import, the HSN/SAC field in Add Product autocompletes. CSV columns:
code, description, gstRate. - PIN codes — postal codes with area, district, and state. After import, the company address PIN field autofills city/state. CSV columns:
pincode, area, district, state. - IFSC codes — bank branch lookup. Adds a standalone IFSC search box in Settings. CSV columns:
ifsc, bank, branch, address, district, state.
Each import replaces the existing rows for that dataset — re-importing is always safe. None of these imports are required; the app works fully without them.
Appearance
- Theme toggle — switch between a dark and a light theme from the sun/moon icon in the header. The choice is remembered per device (stored in the browser, not on the account) and defaults to your system preference on first visit.
Account
- Change password — available to all users from the account menu in the top-right corner.
- Logout — invalidates the session cookie immediately.
Backup, Update & Reset
Your data lives in a Docker volume. Here's how to back it up, update the app, and start fresh when needed.
Where your data lives
All data — the SQLite database (pos.db) and the auto-generated JWT signing secret — lives in a Docker named volume called nodedr-pos_data. This is separate from the container filesystem.
This means rebuilding, recreating, or removing containers never touches your data. You can run docker compose down and docker compose up -d any number of times and your products, invoices, customers, and settings are always there. Run docker volume ls to confirm the volume exists.
Backing up your data
Since the volume is managed by Docker, you cannot just copy a folder. Use a throwaway container to extract the database file:
docker run --rm \
-v nodedr-pos_data:/data:ro \
-v "$PWD":/backup \
alpine cp /data/pos.db /backup/pos-backup-$(date +%Y%m%d).dbThis drops a timestamped copy of pos.db in your current directory. The :ro flag mounts the volume read-only — the running app is not affected.
Scheduled backups
To automate backups, add a cron job on the host machine:
# Run daily at 2am, keep 30 days of backups
0 2 * * * docker run --rm -v nodedr-pos_data:/data:ro -v /path/to/backups:/backup alpine sh -c "cp /data/pos.db /backup/pos-backup-\$(date +\%Y\%m\%d).db && find /backup -name 'pos-backup-*.db' -mtime +30 -delete"Restoring from backup
# 1. Stop the stack
docker compose down
# 2. Copy your backup into the volume
docker run --rm \
-v nodedr-pos_data:/data \
-v "$PWD":/backup \
alpine cp /backup/pos-backup-20260720.db /data/pos.db
# 3. Restart
docker compose up -dUpdating to a new version
# From inside your nodedr-pos directory:
git pull
docker compose up -d --buildOr re-run ./install.sh — it does exactly the same thing. Your data in the volume is untouched. The build is cached from the previous run, so only changed layers are rebuilt.
Resetting / clearing data
Full reset (wipe everything)
Removes all data — admin account, shop settings, products, invoices, customers — and returns you to the onboarding wizard.
# Stop the stack AND delete the volume
docker compose down -v
# Start fresh — onboarding runs on first open
docker compose up -d-v flag deletes the nodedr-pos_data volume and all data inside it. Make a backup first if you need to recover anything.Remove volume without stopping containers
# Stop first, then remove the volume
docker compose down
docker volume rm nodedr-pos_dataClear only catalog and sales (keep admin + settings)
There is currently no one-command way to do a partial reset that preserves admin credentials and shop settings. To do this, delete products and invoices from within the app (Inventory → delete products, Sales → History → delete individual invoices) or connect to the SQLite database directly and truncate specific tables.
Connecting to the database directly
# Start a shell in a container with the volume mounted
docker run --rm -it \
-v nodedr-pos_data:/data \
alpine sh
# Inside the container, install sqlite3 and open the database
apk add sqlite
sqlite3 /data/pos.db
# Useful SQLite commands:
.tables -- list all tables
.schema Invoice -- show table schema
SELECT COUNT(*) FROM Invoice;
.quitAPI Reference
All endpoints are under /api and accessed through the frontend proxy at http://<machine>:1994/api. The backend is never directly reachable.
Authentication
All routes except /auth/status, /auth/login, /auth/register, and the one-time POST /settings require the nodedr_session HttpOnly cookie. Admin-only routes additionally require the admin role on the authenticated account.
To authenticate: POST /api/auth/login with { "email": "...", "password": "..." }. The session cookie is set automatically on success and included in all subsequent requests by the browser.
Base URL
http://<machine>:1994/apiAll requests go to the Next.js frontend, which server-side proxies /api/* to the internal backend. The backend is never directly accessible from the network.
Response format
All responses are JSON. Errors return a { "error": "message" } body with an appropriate HTTP status code (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Validation Error, 500 Internal Server Error).
Endpoint table
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/auth/status | None | Whether an admin account has been created yet (drives onboarding redirect) |
| POST | /api/auth/register | None | Create the first admin account (one-time, disabled once an admin exists) |
| POST | /api/auth/login | None | Authenticate with email + password; sets nodedr_session cookie |
| POST | /api/auth/logout | Session | Invalidate the current session cookie |
| POST | /api/auth/change-password | Session | Change the authenticated user's password |
| GET | /api/auth/users | Admin | List all staff accounts |
| POST | /api/auth/users | Admin | Create a new staff account |
| PUT | /api/auth/users/:id | Admin | Update a staff account (name, role, active state, password reset) |
| GET | /api/settings | Session | Read shop settings (currency, tax, loyalty, receipt config) |
| POST | /api/settings | None | Initial onboarding — create shop settings (one-time) |
| PUT | /api/settings | Admin | Update shop settings |
| GET | /api/products | Session | List all products; supports ?search=&category= query params |
| POST | /api/products | Admin | Create a new product |
| GET | /api/products/:id | Session | Get a single product by ID |
| PUT | /api/products/:id | Admin | Update a product |
| DELETE | /api/products/:id | Admin | Delete a product |
| GET | /api/products/barcode/:code | Session | Look up a product by barcode |
| GET | /api/products/low-stock | Session | Products at or below the low-stock threshold |
| GET | /api/customers | Session | List customers; supports ?search= and ?page= params |
| POST | /api/customers | Session | Create a customer |
| PUT | /api/customers/:id | Session | Update a customer |
| GET | /api/customers/phone/:phone | Session | Look up a customer by phone number |
| GET | /api/customers/top-loyalty | Session | Top customers ranked by current loyalty point balance |
| POST | /api/customers/:id/settle-due | Session | Record a payment against a customer's due balance |
| GET | /api/customers/:id/due-payments | Session | List all due payments for a customer |
| POST | /api/invoices | Session | Finalize a sale — server computes price, tax, discount, loyalty. Can include returns for exchange. |
| GET | /api/invoices | Session | Paginated sales history; supports ?search=&from=&to=&paymentMethod= params |
| GET | /api/invoices/summary | Session | Dashboard stat cards — today's revenue, count, average order value |
| GET | /api/invoices/analytics | Session | Chart data — revenue trend, top products, payment method mix |
| GET | /api/invoices/export.csv | Admin | Full sales history as CSV; supports ?from=&to= date range |
| GET | /api/invoices/:id | Session | Get a single invoice with all line items |
| POST | /api/returns | Session | Standalone return against a past invoice — restocks items, issues refund or store credit |
| GET | /api/returns/by-invoice/:invoiceId | Session | All returns made against an invoice (used to compute remaining returnable quantity) |
| GET | /api/print/:invoiceId/receipt | Session | Self-printing HTML receipt — opens a page and calls window.print() |
| GET | /api/print/:invoiceId/pdf | Session | Server-generated PDF receipt download |
| GET | /api/masters/summary | Admin | Row counts for all reference data tables |
| POST | /api/masters/tax-codes/import | Admin | Import HSN/SAC CSV — replaces existing rows |
| GET | /api/masters/tax-codes/search | Session | Autocomplete HSN/SAC codes; ?q=&type=HSN|SAC |
| POST | /api/masters/pincodes/import | Admin | Import PIN code CSV — replaces existing rows |
| GET | /api/masters/pincodes/:code | Session | Look up a PIN code for city/state autofill |
| POST | /api/masters/ifsc/import | Admin | Import IFSC code CSV — replaces existing rows |
| GET | /api/masters/ifsc/:code | Session | Look up an IFSC code for bank/branch details |
| GET | /api/api-keys | Admin | List External Stock API integrations (no secrets returned) |
| POST | /api/api-keys | Admin | Create an integration — returns the plaintext key once |
| PUT | /api/api-keys/:id | Admin | Rename, change write access, change webhook URL, or revoke |
| DELETE | /api/api-keys/:id | Admin | Permanently delete an integration |
Example — finalize a sale
POST /api/invoices
Content-Type: application/json
Cookie: nodedr_session=<your-session-cookie>
{
"customerId": 42,
"items": [
{ "productId": 7, "quantity": 2 },
{ "productId": 15, "quantity": 1 }
],
"paymentMethod": "UPI",
"amountPaid": 250.00,
"discountPercent": 5,
"loyaltyRedeemed": 10.00
}
// The server computes final price, tax, discount, loyalty deduction
// from the catalog and settings — client values are never trusted for money.Example — export sales CSV
GET /api/invoices/export.csv?from=2026-07-01&to=2026-07-25
Cookie: nodedr_session=<admin-session-cookie>
# Returns Content-Type: text/csv with filename attachment headerExternal Stock API
Connect an e-commerce storefront (or any external system) to a shop's live inventory — read stock, and optionally report sales back — without giving it access to anything else.
This is a separate, key-authenticated surface from the rest of the API above — an external system authenticates with an API key, not a session cookie, and can only ever reach product stock. Set it up in Settings → Integrations: create a key, choose read-only or read + write, optionally give it a webhook URL, then link the products you want exposed by setting their SKU (Inventory → edit product).
Authentication
Every request needs Authorization: Bearer <api key>. Keys are shown once, at creation, and stored hashed — there is no way to retrieve a lost key, only issue a new one.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/external/products | API Key | Every product with a SKU linked: sku, name, stock, unit, sellingPrice, taxRate |
| GET | /api/external/products/:sku | API Key | Live stock for a single SKU |
| PATCH | /api/external/products/:sku/stock | API Key | Adjust stock — { delta } or { set }, write-scoped keys only |
Example — report a sale made on your storefront
PATCH /api/external/products/MUG-001/stock
Authorization: Bearer nk_live_...
Content-Type: application/json
{
"delta": -2,
"idempotencyKey": "order-4821-line-1"
}
// delta is relative ("2 units just sold"); "set" (an absolute count) also
// works. idempotencyKey makes a retried call safe to send again — a
// repeated key returns the original result instead of applying it twice.Webhook — live stock pushed to you
If a key has a webhook URL configured, the shop POSTs it whenever a linked product's stock changes there — a counter sale, a return, or a manual edit — so a storefront can mirror stock live instead of only polling.
POST <your webhook URL>
X-Nodedr-Signature: sha256=<hex>
Content-Type: application/json
{
"event": "stock.updated",
"timestamp": "2026-08-26T12:00:00.000Z",
"changes": [{ "sku": "MUG-001", "stock": 14 }]
}
// Verify the signature: HMAC-SHA256 of the raw request body, using the
// webhook secret shown once when the webhook URL was set.Security Model
NodeDR POS is designed for a trusted local network. Here's exactly what protections are in place.
Design intent
NodeDR POS is designed for a trusted local network — a shop's private LAN or a single machine. The default deployment is HTTP. If you expose it beyond the counter (e.g. over the internet or a VPN), you should terminate HTTPS in front of it with a reverse proxy and set COOKIE_SECURE=true in the backend environment.
Passwords
- All passwords are hashed with bcrypt at cost factor 12. Plaintext is never stored or logged.
- Login returns an identical error for unknown email vs. wrong password — no user enumeration is possible.
- Login is rate-limited to 10 attempts per 15 minutes per IP, on top of a global rate limiter (300 req/min across all routes). After exceeding the limit, the client receives HTTP 429 and must wait before retrying.
Sessions
- Sessions use HttpOnly, SameSite=Lax JWT cookies signed with HS256 and a 256-bit secret auto-generated on first boot.
- The algorithm is pinned to HS256 in the verify call — algorithm confusion attacks are not possible.
- Every authenticated request re-checks that the account still exists and is active in the database — disabling a staff member logs them out immediately on their next request, not just on next login.
- The JWT signing secret is stored in the
nodedr-pos_datavolume with mode 600 — never in the repo or build image.
Authorization
- All data routes require authentication.
- Settings management, staff management, CSV export, and reference data imports require the admin role.
- The last active admin cannot be demoted or disabled — you cannot lock yourself out.
- Authorization is enforced server-side on every request — the frontend UI hiding a button is never the only protection.
Server-authoritative money
This is the most important security property of the checkout flow. The client sends only product IDs, quantities, and intent (e.g. “pay with UPI”, “redeem 50 loyalty points”). The server then:
- Looks up current prices from the catalog — a client cannot submit a manipulated price.
- Computes tax, discount, and loyalty value from the current Settings — a client cannot override these.
- Caps loyalty redemption at the customer’s actual balance.
- Caps the per-sale discount at the configured maximum.
- Decrements stock in the same transaction as creating the invoice — no race condition between “check stock” and “create invoice”.
A tampered request from the browser cannot alter what a sale charges or how much stock is decremented.
Input validation
- Every write endpoint validates with Zod — allowlisted fields only, no mass assignment.
- All database access is through Prisma ORM with parameterized queries — SQL injection is not possible.
- CSV imports are admin-only, capped at 25MB, and parsed in-memory (no temp files, no shell-out).
- HTTP response headers include
X-Content-Type-Options,X-Frame-Options, and other security headers viahelmet.
Network exposure
- Only one port (1994) is exposed to the network — the Next.js frontend.
- The Express backend runs on the internal Docker network only — it is not reachable from the LAN.
- All
/api/*traffic goes through the Next.js server-side proxy — the backend URL is never in the browser bundle. - Neither container runs with
privileged: true. The USB passthrough for ESC/POS printing uses a scopeddevice_cgroup_rulesentry for USB device major 189 only.
No external calls at runtime
Once the images are built, NodeDR POS makes zero outbound network calls. No analytics, no telemetry, no version-check pings, no external CDN for assets. Your sales data and customer information never leave the machine running Docker.
Reporting a vulnerability
If you discover a security issue in NodeDR POS, please report it by opening a GitHub issue with the label security, or email info@nodedr.com. We take security reports seriously and will respond within 48 hours.
Windows Distribution & Store Submission
How the native Windows installer is built, and exactly what to enter when submitting it to the Microsoft Store.
Code signing status
The installer is not yet code-signed. Windows SmartScreen shows “Windows protected your PC” on first run until it builds download reputation, and — more importantly — the Microsoft Store requires an EXE/MSI submission’s installer to chain to a Microsoft Trusted Root Program CA. Signing is scaffolded via the free SignPath Foundation program (this project is AGPL-3.0, public, and OSI-approved-licensed, which qualifies) but not yet active — don’t submit to the Store until a signed build exists.
Package details — exact values
- Package URL:
https://pos.nodedr.com/downloads/nodedr-pos-setup-1.0.0-x64.exe— hosted directly on this site (baked into the Docker image at build time from the pinned release asset, checksum-verified; see the Dockerfile), not linked to GitHub. GitHub’sreleases/download/...URLs 302-redirect throughobjects.githubusercontent.com, which Microsoft Store Package URL validation rejects outright. This same-origin URL is also version-pinned (not/latest/) for the same reason a versioned GitHub URL would have been: the Store shouldn’t be able to silently pick up a different, unreviewed build on a later re-validation pass. (The site’s own download button above intentionally keeps using GitHub’s/latest/alias instead — that’s a different, correct use case: always serving the newest release to a visitor without needing a site redeploy.) - Architecture: x64 only — no 32-bit, ARM, or ARM64 build exists.
- App type: EXE (NSIS-built, not MSI).
- Installer parameters:
/S— the standard NSIS silent switch, exercised by CI’s own install step on every build. - Languages: English only — the app has no locale-switching UI.
- Installer handling / return codes: leave scenario-specific codes blank except Installation successful →
0. The installer script has no custom per-scenario exit codes for cases like disk-full or already-installed — only NSIS’s generic success/abort behavior.
How the installer is built
Built on a real Windows GitHub Actions runner (not cross-compiled) — better-sqlite3 and usb are native Node addons whose binaries are platform- and ABI-specific, and building on Windows means the result gets installed, exercised end-to-end (register → sale → print → service restart → uninstall), and uninstalled before anyone downloads it. See packaging/windows/README.md and .github/workflows/build-windows-installer.yml in the repository for the full build/CI detail.
Contributing
Contributions are welcome. NodeDR POS is a focused tool — please keep PRs aligned with the offline-first single-shop POS mission.
Project scope
NodeDR POS is intentionally a focused, offline-first, single-shop POS. Please keep contributions aligned with this goal rather than expanding into multi-tenant, cloud, or SaaS territory. Good contributions include:
- Bug fixes and stability improvements.
- Hardware compatibility (new printer types, scanner edge cases).
- Localization / currency support for additional countries.
- UX improvements to the register, inventory, or reports flows.
- Performance improvements (query optimization, startup time).
- Documentation fixes and improvements.
Out-of-scope (please discuss in an issue first): cloud sync, multi-branch, SaaS billing, OAuth login, mobile native apps.
Reporting bugs
Open a GitHub issue with:
- NodeDR POS version (the commit or git tag).
- Your OS and Docker version (
docker compose version). - Steps to reproduce.
- What you expected vs. what happened.
- Output of
docker compose logsif there are backend errors.
Local development setup
You can run the backend and frontend without Docker for faster iteration:
# Terminal 1 — backend on :4000
cd backend
cp .env.example .env
npm install
npm run prisma:migrate:dev # creates the SQLite database
npm run dev # starts Express with nodemon
# Terminal 2 — frontend on :1994
cd frontend
npm install
BACKEND_URL=http://localhost:4000 npm run devOpen http://localhost:1994. The Next.js dev server proxies /api/* to BACKEND_URL. No API URL ever ends up in the browser bundle.
Database schema changes
The schema lives in backend/prisma/schema.prisma. After editing it:
cd backend
npm run prisma:migrate:dev -- --name your-migration-nameThis creates a migration file under backend/prisma/migrations/. Commit both the schema and migration file.
Submitting a pull request
- Fork the repo and create a branch from
main:git checkout -b fix/your-bug-name. - Make your changes. Keep commits focused — one logical change per commit.
- Test manually: run the Docker stack and verify the change works end-to-end.
- Open a PR against
mainwith a clear description of what changed and why. - Reference the issue number in the PR description if applicable.
Code style
- Backend — Node.js / Express / Prisma. Follow the existing style (no TypeScript on the backend — it uses
.jswith JSDoc types where needed). - Frontend — Next.js App Router, React, TypeScript, Tailwind CSS. Follow the existing component patterns. Server Components by default;
use clientonly where interaction requires it. - No linter enforced yet — match the surrounding code style.
License
By contributing, you agree that your contributions will be licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), the same license as the project.