Documentation

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.

AGPL-3.0DockerNext.jsSQLiteESC/POSGST-ready

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:

  1. Install NodeDR POS — one command, under 10 minutes.
  2. Set up your hardware — barcode scanner and thermal printer.
  3. 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 including Hong Kong dollar (HK$), 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.
  • Find products online — search free product databases by name or barcode, validate the data, review it, and add it; unknown barcodes auto-fill on scan.
  • Customer display — a live second screen on any phone or tablet, with the UPI payment QR.
  • Online orders — click-and-collect board for your website, WooCommerce or Shopify; connect your own site through the API.
  • Suppliers & purchase orders — reorder suggestions, hand-made orders, PDFs, receiving into stock.
  • One-click updates — Settings → Updates on Docker and CasaOS.

Browse the documentation

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 volume

The 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.

Getting Started

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 | bash

Windows (PowerShell):

irm https://raw.githubusercontent.com/Raktim94/nodedr-pos/master/scripts/quickstart.ps1 | iex

See 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 version to check — you need v2.x, not the legacy docker-compose command.
  • 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.sh

The install.sh script does the following automatically:

  1. Checks that Docker and Compose are installed and healthy.
  2. Runs docker compose build — builds both the frontend and backend images from source using multi-stage node:24-alpine builds. First run takes 3–5 minutes; subsequent runs are cached and take under a minute.
  3. Runs docker compose up -d — starts both containers in the background and creates the nodedr-pos_data named volume on first run.
  4. Polls the backend health endpoint until it reports healthy.
  5. Prints http://localhost:1994 and 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:1994

First 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:

  1. Admin account — your full name, email address, and a password. This becomes the owner/admin login.
  2. 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:2000

The 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=true

Put 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>:1994

Find 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 -v
Data safety: all shop data (SQLite database, session secret) lives in the nodedr-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 --build

Or 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.

  1. In CasaOS/ZimaOS, go to App Store → + (top right) → Install a customized app (or the equivalent Custom Install option on ZimaOS).
  2. Paste this compose URL:
    https://raw.githubusercontent.com/Raktim94/nodedr-pos/master/casaos/docker-compose.yml
  3. Confirm the install-time configuration CasaOS shows you — web UI port 1994, the data directory, and the optional USB printer passthrough — then click Install.
  4. 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: Microsoft Store (no Docker)

If you would rather not install Docker, install NodeDR POS from the Microsoft Store. It updates itself through the Store, runs only while its window is open, and is not reachable from other devices on the network. For a till that counter tablets or phones on the shop LAN must reach, use the one-command install above.

We no longer publish downloadable installer files (.exe, .msix, .deb or .dmg). On Linux and macOS, use the one-command install at the top of this section.

Getting Started

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, add http://<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 caddy service ships in docker-compose.yml behind the https Compose profile, off by default:
    docker compose --profile https up -d
    Then open https://<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–29 prefix 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 only

This 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.

Limitation: generic ESC/POS printers default to single-byte codepage (CP437), not UTF-8. Accented Latin letters are transliterated (café → cafe) and non-ASCII currency symbols fall back to 3-letter codes (€ → EUR). Non-Latin scripts (Devanagari, Arabic, CJK) print as ?. 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/usb passthrough is a Linux feature). macOS and Windows users should use CUPS / Windows print spooler with the browser print button instead.
Using NodeDR POS

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:

  1. Re-computes the price, tax, discount, and loyalty redemption from the catalog and current settings — client values are never trusted for money.
  2. Decrements stock for each sold item.
  3. Creates the invoice, invoice lines, and any due/loyalty transactions.
  4. 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.
Using NodeDR POS

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, NodeDR POS looks it up in the free online product databases and opens the Add Product form pre-filled (name, category, photo) with a note to review before saving. If nothing is found, or the shop is offline, the form simply opens with the barcode filled in — the fastest workflow for onboarding new products at the counter.

Find products online

Click Find online on the Inventory page and search by product name or barcode. Data comes from the free community databases Open Food Facts, Open Beauty Facts and Open Products Facts. Only the text you search (or the barcode) leaves your server — never shop data.

  • Each result shows a photo, name, brand, size, category and barcode. Products you already stock are marked In catalog.
  • Results are validated: the barcode check digit is verified (a mistyped or corrupt code gets a warning), entries with no real name are dropped, a record whose code differs from the one you asked for is discarded, and missing photo/category are flagged.
  • Review & add opens the normal Add Product form pre-filled. Nothing is saved until you press Save. Prices are not included in this data — enter your purchase and selling price.
  • If nothing matches you are told so; if the databases are unreachable you get a clear message and can add the product by hand.

Check online & replace details

In any product form (Add or Edit), click Check online under the photo. It compares your name, category and photo with the online record for that barcode. Each field has its own Use button — replace only what you want. Same appears where they already match. Nothing changes until you press Save.

Product photos

Add a photo in the Add or Edit form — pick a file, or take one with the phone camera. Photos are resized in the browser and stored on your own server (PNG, JPEG or WebP, checked on upload). They appear as thumbnails in the inventory list. Remove deletes the photo.

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:

  1. Open the Add Product form.
  2. Click Generate next to the Barcode field.
  3. A valid EAN-13 is produced using the 20–29 GS1 internal-use prefix range. It is checked against your catalog for uniqueness and retried automatically if there is a collision.
  4. 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, discountPercent

Only name and price are required. The import replaces matching barcodes and inserts new rows — it is safe to re-import an updated file.

Using NodeDR POS

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 (A$), CAD (C$), SGD (S$), HKD (HK$), JPY (¥), 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:

  1. Download the current HSN/SAC CSV from the official tax authority in your country (e.g. CBIC in India).
  2. Go to Settings → Reference Data → Import Tax Codes (admin only).
  3. Upload the CSV. Expected columns: code, description, gstRate (rate is optional).
  4. 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.

Using NodeDR POS

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

  1. Attach a customer (phone number required) at the POS.
  2. At checkout, select Cash as the payment method.
  3. Enter an Amount received less than the sale total.
  4. The system warns before finalizing that the shortfall will be added to the customer’s due.
  5. 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

  1. Go to Customers and click the customer.
  2. Click the due amount to open the payment form.
  3. Enter the amount received (capped at current balance).
  4. The payment is recorded in the customer’s due payment history (CustomerDuePayment table) — 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.

Using NodeDR POS

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

  1. Open the POS screen.
  2. Click Return / Exchange (or press the keyboard shortcut).
  3. Enter the invoice number (e.g. INV-2026-00047) and press Enter.
  4. The original sale lines appear. Select the items and quantities to return.
  5. 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.
  6. Choose the refund method: Cash, UPI, Card, or Store Credit.
  7. 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:

  1. Follow steps 1–5 above to queue the return lines.
  2. Without finalizing, add new items to the cart as you would for a regular sale.
  3. 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.
  4. Finalize in one transaction. The return is recorded with refundMethod: BILL_OFFSET for 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.

Using NodeDR POS

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/usb passthrough — 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/usb is already bind-mounted and cgroup rules are set in docker-compose.yml.
Unicode limitation: ESC/POS printers default to single-byte codepage (CP437). Accented Latin (café → cafe) is transliterated. Non-ASCII currency symbols fall back to 3-letter codes (€ → EUR). Non-Latin scripts (CJK, Arabic, Devanagari) print as ?. 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-NNNNN format.
  • 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.

Using NodeDR POS

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

Four tiles across the top:

  • Today’s revenue — sales finalized today, with the change against yesterday.
  • Today’s bills — how many sales were rung up today.
  • Low stock — products at or below your low-stock threshold, with a link straight to Inventory.
  • Customer dues — the total unpaid balance across customers, and how many owe it.

Revenue, last 14 days

A bar per day with today highlighted. Hover a bar for the exact amount and number of bills. Drawn in the browser — no external chart service. A progress ring beside it shows how today compares with your usual daily revenue.

Lists you act on

  • Low inventory — the products running out, with units left.
  • Online orders waiting — open orders from your e-commerce store with pickup code, item count and status (shown if your role can see orders). It refreshes every 15 seconds and when you return to the tab, so a card moved on the Online orders board shows here without a reload.
  • Customers with dues — who owes the most.

Charts

Below the tiles, charts for revenue, top products and the payment mix (cash, UPI, card).

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.

Using NodeDR POS

Customer Display

Show the customer their items, the total and the UPI payment QR on a phone, tablet or second screen — wirelessly, no extra software.

Open it on another device

  1. On the POS page press Customer display.
  2. Open the link it shows on the phone, tablet or second PC on the same Wi-Fi/LAN — or scan its QR code. The link looks like http://<machine>:1994/display?key=….
  3. That’s it. The display follows the till automatically (about once a second) and reconnects by itself if the Wi-Fi drops. The POS shows how many displays are connected.

What the customer sees

  • The cart as items are scanned, with discounts, tax and the total.
  • When the cashier picks UPI, a large payment QR for the exact amount (including any previous balance) — the customer scans it with any UPI app. For shops with an INR currency and a UPI id set.
  • A “Thank you” screen after the sale.

Privacy

The key in the link is a private secret generated on your server. Anyone who has the link can watch the cart, so share it only with your own screens. A wrong or missing key shows an “invalid link” message. On a tablet, use the browser’s fullscreen or “Add to Home screen” for a kiosk look.

Payment confirmation sound

When a sale is completed the till plays a short chime — and speaks “Payment received” with the amount for UPI and card sales. Turn it on or off with Payment sound on the POS page. The cashier confirms the sale; automatic confirmation straight from the bank is on the roadmap.

Using NodeDR POS

Suppliers & Purchasing

Keep your suppliers, see what is running low, and send purchase orders — receive the goods straight into stock.

Suppliers

Open Purchasing → Suppliers. Add a supplier (name, phone, email, address, GSTIN, notes), click the pencil on a card to edit it, or delete it from the edit window (products that used it become unassigned; a supplier with purchase orders cannot be deleted).

Reorder suggestions

Set a reorder point and a supplier on a product. When stock falls to the reorder point it appears under Purchasing → Reorder, grouped by supplier with a suggested quantity. One click creates a purchase order.

Purchase orders

  • New purchase order — choose a supplier, search and add products, set quantity and unit cost, add a note, and create it.
  • Mark sent, download the PDF to send to the supplier, or cancel a draft or sent order.
  • Receive — enter what arrived (partial deliveries are fine). Stock goes up and the latest purchase cost is saved on the product, which feeds your margin reports.
Using NodeDR POS

Online Orders

Orders from your website or marketplace land on a board at the counter. Drag a card across and your store is told — no retyping, no second system.

The Online orders board: New, Packing, Ready and Collected columns

Where orders come from

Orders arrive from an e-commerce store: your own site through the External API, or a WooCommerce / Shopify / generic store connected in Settings → Online stores. Items are matched to products by SKU. Each order gets a 6-character pickup code with no look-alike characters (no 0/O or 1/I), so it can be read aloud or scanned.

The board

  • New — stock is reserved for the order, not yet sold.
  • Packing — someone is picking it.
  • Ready — waiting for the customer (or the courier).
  • Collected — handed over (and billed in the POS if it was a pay-at-pickup order).

Drag a card to the next column, or use the button on the card — same result, and it works with a tap on a tablet. Cancel with the red ✕. Orders only move forward; the board refuses a jump back and tells you why.

Stock is reserved, then sold

An open order reserves stock instead of removing it: the API reports available (stock minus reservations) so your site never oversells. The stock deduction happens once, at Hand over (or by typing/scanning the pickup code into the Collect box). Cancelling needs no stock unwinding.

Who bills the customer?

Billing follows who took the money, so a sale is never counted twice:

  • Paid on the website (paid: true) — Hand over takes the items out of stock and tells your site; no POS bill is made, because your website already issued the invoice.
  • Pay at pickup (paid: false) — Hand over & bill collects cash, UPI or card at the counter and the POS issues the normal GST bill (IMEI/serial numbers are captured here if you use them).
  • The hand-over dialog has a Bill this order in the POS checkbox, so the cashier can switch either way at the counter.

Connect your own website

Built a custom store (plain HTML + a backend, Next.js, Laravel, Django, an app)? It can connect with the External API: create an API key in Settings → Integrations, give the products you sell online a SKU, send each order to POST /api/external/orders, and receive signed order.updated webhooks as the shop packs and hands over. The Settings → Online stores page has a ready-to-copy example, and the full guide with Node and Python code is on GitHub: docs/CUSTOM_STORE.md.

How your store finds out

Every change on the board is sent out the moment you make it:

  • API key store — a signed order.updated webhook with the new status and the previousStatus, retried 4 times if your site is briefly down. Or poll GET /api/external/orders?updatedSince=…. Details in External API & Webhooks.
  • WooCommerce — the order is set to processing / completed / cancelled, and the customer gets a note with the pickup code when it is Ready.
  • Shopify — the order is tagged nodedr-packing, nodedr-ready or nodedr-collected (so you can drive flows and emails from it), and cancelled when you cancel it.

WooCommerce and Shopify pushes need the store’s outbound credentials saved on the integration, and were written against each platform’s public API — try them on a staging store first. A failed push is logged and never blocks the till.

Administration

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

  1. Log in as admin.
  2. Go to Settings → Staff.
  3. Click Add staff member.
  4. Enter the name, email, and initial password.
  5. Select role: Cashier (or Admin if you want to grant full access).
  6. 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.

Administration

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.
Administration

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.

One-click updates (Docker & CasaOS)

Go to Settings → Updates. It shows the installed and the latest version; click Check for updates to look now. When a newer version is published, click Update now and confirm your password. A small helper container pulls the new images and restarts the app — usually under a minute. Your data is never touched: it lives in a volume (or in /DATA/AppData/nodedr-pos on CasaOS), separate from the containers. If an update fails, the old version is put back and the reason is shown.

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).db

This 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 -d

Updating to a new version

# From inside your nodedr-pos directory:
git pull
docker compose up -d --build

Or 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
This is irreversible. The -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_data

Clear 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;
.quit
Developer

API 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/api

All 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

MethodPathAuthDescription
GET/api/auth/statusNoneWhether an admin account has been created yet (drives onboarding redirect)
POST/api/auth/registerNoneCreate the first admin account (one-time, disabled once an admin exists)
POST/api/auth/loginNoneAuthenticate with email + password; sets nodedr_session cookie
POST/api/auth/logoutSessionInvalidate the current session cookie
POST/api/auth/change-passwordSessionChange the authenticated user's password
GET/api/auth/usersAdminList all staff accounts
POST/api/auth/usersAdminCreate a new staff account
PUT/api/auth/users/:idAdminUpdate a staff account (name, role, active state, password reset)
GET/api/settingsSessionRead shop settings (currency, tax, loyalty, receipt config)
POST/api/settingsNoneInitial onboarding — create shop settings (one-time)
PUT/api/settingsAdminUpdate shop settings
GET/api/productsSessionList all products; supports ?search=&category= query params
POST/api/productsAdminCreate a new product
GET/api/products/:idSessionGet a single product by ID
PUT/api/products/:idAdminUpdate a product
DELETE/api/products/:idAdminDelete a product
GET/api/products/barcode/:codeSessionLook up a product by barcode
GET/api/products/low-stockSessionProducts at or below the low-stock threshold
GET/api/customersSessionList customers; supports ?search= and ?page= params
POST/api/customersSessionCreate a customer
PUT/api/customers/:idSessionUpdate a customer
GET/api/customers/phone/:phoneSessionLook up a customer by phone number
GET/api/customers/top-loyaltySessionTop customers ranked by current loyalty point balance
POST/api/customers/:id/settle-dueSessionRecord a payment against a customer's due balance
GET/api/customers/:id/due-paymentsSessionList all due payments for a customer
POST/api/invoicesSessionFinalize a sale — server computes price, tax, discount, loyalty. Can include returns for exchange.
GET/api/invoicesSessionPaginated sales history; supports ?search=&from=&to=&paymentMethod= params
GET/api/invoices/summarySessionDashboard stat cards — today's revenue, count, average order value
GET/api/invoices/analyticsSessionChart data — revenue trend, top products, payment method mix
GET/api/invoices/export.csvAdminFull sales history as CSV; supports ?from=&to= date range
GET/api/invoices/:idSessionGet a single invoice with all line items
POST/api/returnsSessionStandalone return against a past invoice — restocks items, issues refund or store credit
GET/api/returns/by-invoice/:invoiceIdSessionAll returns made against an invoice (used to compute remaining returnable quantity)
GET/api/print/:invoiceId/receiptSessionSelf-printing HTML receipt — opens a page and calls window.print()
GET/api/print/:invoiceId/pdfSessionServer-generated PDF receipt download
GET/api/masters/summaryAdminRow counts for all reference data tables
POST/api/masters/tax-codes/importAdminImport HSN/SAC CSV — replaces existing rows
GET/api/masters/tax-codes/searchSessionAutocomplete HSN/SAC codes; ?q=&type=HSN|SAC
POST/api/masters/pincodes/importAdminImport PIN code CSV — replaces existing rows
GET/api/masters/pincodes/:codeSessionLook up a PIN code for city/state autofill
POST/api/masters/ifsc/importAdminImport IFSC code CSV — replaces existing rows
GET/api/masters/ifsc/:codeSessionLook up an IFSC code for bank/branch details
GET/api/api-keysAdminList External Stock API integrations (no secrets returned)
POST/api/api-keysAdminCreate an integration — returns the plaintext key once
PUT/api/api-keys/:idAdminRename, change write access, change webhook URL, or revoke
DELETE/api/api-keys/:idAdminPermanently 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 header
Developer

External API & Webhooks

Connect an e-commerce store, an app or an AI agent to the shop: read stock, take bills, create orders, and hear about every status change — with a key that can do only what you allow.

A separate, key-authenticated surface from the session API above — an external system authenticates with an API key, never a cookie. Create one in Settings → Integrations, tick the permissions it needs, optionally give it an https:// webhook URL, then link the products to expose by setting their SKU (Inventory → edit product). A product with no SKU is invisible to the API.

Authentication & permissions

Every request needs Authorization: Bearer nk_live_…. Keys are shown once and stored hashed — a lost key is replaced, not recovered. Each key carries scopes, and an endpoint outside them returns 403:

ScopeAllows
products:readList and look up SKU-linked products with live stock and price
stock:writeAdjust stock
bills:writeTake bills — the POS computes price, GST, discount and stock
bills:readRead bills created through the same key, and download their PDFs
orders:writeCreate, read, list and cancel click-and-collect / delivery orders
warranty:readWarranty status from an IMEI / serial number

Endpoints

MethodPathAuthDescription
GET/api/external/productsAPI KeySKU-linked products with stock, available (stock minus open-order reservations), price and tax. ?limit, ?offset, ?updatedSince, ?q
GET/api/external/products/:skuOrBarcodeAPI KeyOne product, live
PATCH/api/external/products/:sku/stockAPI KeyAdjust stock — { delta } or { set }, optional idempotencyKey (scope stock:write)
POST/api/external/billsAPI KeyTake a bill — the POS computes price, GST, discount and stock. Idempotent on externalRef (scope bills:write)
GET/api/external/bills/:refAPI KeyA bill made with this key, by invoice number or externalRef (scope bills:read)
GET/api/external/bills/:ref/pdfAPI KeyPDF bill — ?layout=a4 or receipt (scope bills:read)
POST/api/external/ordersAPI KeyCreate a click-and-collect or delivery order; reserves stock and returns a pickupCode. Idempotent on externalId (scope orders:write)
GET/api/external/ordersAPI KeyPoll your own orders — ?status=NEW,READY&updatedSince=ISO&limit=100 (scope orders:write)
GET/api/external/orders/:refAPI KeyOne of your orders, by id or externalId, with its current status
POST/api/external/orders/:ref/cancelAPI KeyCancel your own order and release the reserved stock
GET/api/external/warranty/:serialAPI KeyWarranty status from an IMEI / serial (scope warranty:read; needs the IMEI tracking switch)

Write API — create an order from your storefront

Prices always come from the POS catalog; you send SKUs and quantities. The call reserves stock and is idempotent on externalId — a retried request returns the first order (200, deduplicated: true) instead of creating a second.

POST /api/external/orders
Authorization: Bearer nk_live_...
Content-Type: application/json

{
  "externalId": "web-1001",
  "fulfilment": "PICKUP",            // or "DELIVERY"
  "customer": { "name": "Ishita Rao", "phone": "9876500000" },
  "items": [{ "sku": "COLA-1", "quantity": 2 }],
  "paid": true,                      // true = you already charged online (no POS bill at hand-over); false = pay at pickup (POS bills it)
  "note": "Gift wrap"
}

// 201 { "deduplicated": false,
//       "order": { "id": 41, "status": "NEW", "pickupCode": "K7M2QX", "total": 110, ... } }
// 409 when there is not enough available stock.

Write API — take a bill

POST /api/external/bills
Authorization: Bearer nk_live_...

{
  "externalRef": "pos-2231",         // idempotency key
  "customer": { "name": "Asha", "phone": "9999900000" },
  "items": [{ "sku": "MUG-001", "quantity": 2 }],
  "paymentMethod": "UPI"             // UPI / CARD paid in full; CASH uses amountPaid
}

// 201 { "invoice": { ... }, "receiptUrl": "https://shop/.../receipt?token=..." }
// receiptUrl is a signed, 30-day link to the PDF — safe to send to the customer.

Adjust stock

PATCH /api/external/products/MUG-001/stock
Authorization: Bearer nk_live_...

{ "delta": -2, "idempotencyKey": "order-4821-line-1" }

// delta is relative ("2 units just sold"); "set" writes an absolute count.
// A repeated idempotencyKey returns the original result instead of applying it twice.

Webhooks — the shop tells you

Give a key an https:// webhook URL and the shop POSTs it. Each request is signed with X-Nodedr-Signature: sha256=<hex> — the HMAC-SHA256 of the raw body using the secret shown once when you set the URL. Verify it before trusting the call.

  • stock.updated — a linked product’s stock changed at the counter (sale, return, manual edit).
  • order.created and order.updated — only to the key that owns the order; order events carry customer details and are never broadcast to other keys.

Every move on the Online orders board — Packing, Ready, Collected, Cancelled — fires order.updated straight away. If your endpoint is down, delivery is retried 4 times (after 2 s, 6 s, 18 s); after that, GET /api/external/orders?updatedSince=… catches you up.

POST <your webhook URL>
X-Nodedr-Signature: sha256=3f1c...
Content-Type: application/json

{
  "event": "order.updated",
  "timestamp": "2026-10-07T09:30:00.000Z",
  "data": {
    "id": 41,
    "externalId": "web-1001",
    "status": "READY",              // NEW | PACKING | READY | COLLECTED | CANCELLED
    "previousStatus": "PACKING",
    "fulfilment": "PICKUP",
    "pickupCode": "K7M2QX",
    "total": 110,
    "paid": true,
    "updatedAt": "2026-10-07T09:30:00.000Z",
    "items": [{ "name": "Cola 1L", "quantity": 2, "price": 55 }]
  }
}
// Node.js — verify before you trust it
const crypto = require("crypto");
function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return header.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

Reply with any 2xx within 8 seconds. Treat deliveries as at-least-once: key off externalId + status so a repeat does nothing.

WooCommerce and Shopify

Connect a store in Settings → Online stores: you get a webhook URL and signing secret (shown once) to paste into the store. Its orders become pickup/delivery orders matched by SKU; stock changes are pushed back (debounced, one call per SKU every ~2.5 s) and so is every order status change — see Online Orders.

AI agents (MCP)

The same key works against POST https://<shop>/mcp (Streamable HTTP). Tools appear only if the key has the scope: search_products, get_stock, adjust_stock, create_bill, get_bill, create_order, get_order, check_warranty. They call the same services as the REST API, so money rules are identical.

Developer

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_data volume 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 via helmet.

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 scoped device_cgroup_rules entry 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.

Developer

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 logs if 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 dev

Open 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-name

This creates a migration file under backend/prisma/migrations/. Commit both the schema and migration file.

Submitting a pull request

  1. Fork the repo and create a branch from main: git checkout -b fix/your-bug-name.
  2. Make your changes. Keep commits focused — one logical change per commit.
  3. Test manually: run the Docker stack and verify the change works end-to-end.
  4. Open a PR against main with a clear description of what changed and why.
  5. 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 .js with JSDoc types where needed).
  • Frontend — Next.js App Router, React, TypeScript, Tailwind CSS. Follow the existing component patterns. Server Components by default; use client only 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.