Skip to content

Repository files navigation

Offline POS

A point-of-sale application for a small shop, written in C++17 with an embedded SQLite database. It has no server, no cloud account and no network code of any kind. Sales are rung up, stock is deducted and receipts are printed against a single file on the local disk — during a brownout, on a machine that has never been online, or on a laptop behind the counter with the Wi-Fi off.

Built for the Philippine sari-sari store case: peso amounts, VAT-inclusive pricing by default, and a till that must keep working when the internet does not.

========================================================================
Aling Nena Store
========================================================================
Receipt : 20260902-0001
Date    : 2026-09-02 21:53:40
------------------------------------------------------------------------
Coca-Cola 1.5L
  2 x P85.00                                                     P170.00
Skyflakes Crackers
  3 x P9.00                                                       P27.00
------------------------------------------------------------------------
                                                      Subtotal: P197.00
                                                         TOTAL: P197.00
                                                          Cash: P500.00
                                                        Change: P303.00
------------------------------------------------------------------------
Thank you for your purchase!
========================================================================

What "offline-first" means here

Offline-first is a set of design decisions, not a marketing line. In this project it means:

Decision Why
SQLite is vendored in third_party/ The build needs no package manager and no network. A machine with a compiler and this repository can produce a working binary.
The whole shop is one file pos.db holds products, sales, receipts, the stock ledger and the settings. Copy the file, and you have moved or backed up the business.
WAL journaling, synchronous = NORMAL Survives an application crash and a power cut with at most the last transaction at risk, which is what a brownout-prone counter needs.
Checkout is one transaction Stock deduction, sale header, sale lines and ledger entries all commit together, or none of them do. There is no state where stock left the shelf without a receipt.
Stock guards live in the UPDATE UPDATE ... WHERE stock >= ? plus a row-count check, not read-then-write, so two tills cannot both sell the last unit.
Money is integer centavos No floating point anywhere near a price. A till that drifts by a centavo a sale fails its end-of-day count.
Receipt lines are denormalised SKU, name and unit price are copied onto the sale line, so a receipt reprinted in two years shows what the customer actually paid, even after the product is renamed, repriced or deleted.
Every stock change is ledgered stock_movements is append-only, so "where did those twelve units go" is always answerable without a network call.
Static linking on Windows The result is one .exe you can copy to a till. No runtime DLLs to install.

Features

Selling

  • Ring up a sale by SKU (a barcode scanner just types the SKU) or by name search
  • Live cart with per-line quantities, line removal and cart clearing
  • Cash tendering with change calculation, and an explicit short-payment message
  • Optional VAT, rounded half-up to the centavo
  • Sequential daily receipt numbers (YYYYMMDD-0001)
  • Receipt reprint from history

Inventory

  • Add, edit, search, deactivate and delete products
  • Receive stock and correct stock as separate, ledgered operations
  • Low-stock report against a configurable threshold
  • Inventory valuation at current prices
  • Products that appear on past receipts cannot be deleted, only deactivated

Reporting

  • Today's takings, arbitrary date ranges, and per-day totals
  • Best sellers by units and by revenue
  • All-time summary and stock ledger

Operations

  • Online backup to a consistent copy while the database is in use
  • CSV export of products and of sale lines
  • PRAGMA integrity_check and VACUUM from the menu or the command line
  • Store name, currency symbol, VAT rate, receipt footer and low-stock threshold are stored in the database, so a copied file carries its own configuration

Quick start

git clone https://github.com/YOUR-USERNAME/offline-pos.git
cd offline-pos

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release

./build/bin/offline-pos --seed

--seed loads a small sample catalogue so every screen has something in it. Drop it once you have entered real products; seeding twice never duplicates or resets anything.

Building without CMake

CMake is convenient, not required. A compiler and make are enough:

make          # builds build/bin/offline-pos
make test     # builds and runs the test suite
make run      # builds and launches
make clean

On Windows with MSYS2/MinGW, use mingw32-make in place of make. It works from PowerShell and cmd as well as from a Unix shell — the Makefile detects which one it got and picks matching commands, rather than assuming rm -rf is available.

The Windows build links the GCC runtime statically by default, so the executable stands alone with no DLLs to ship beside it. Pass STATIC=0 if you would rather link dynamically.

Requirements

  • A C++17 compiler — GCC 8+, Clang 7+, or MSVC 2019+
  • CMake 3.16+ (optional; the Makefile is a complete alternative)

Nothing else. SQLite is compiled in from third_party/sqlite3/.

Command line

The interactive till is the default, but every operation a backup script needs is available without touching a menu:

offline-pos [options]

  --db <path>            Database file to use (default: pos.db)
  --seed                 Load the sample product catalogue, then continue
  --backup <path>        Write a consistent copy of the database and exit
  --export-products <f>  Write products to a CSV file and exit
  --export-sales <f>     Write sale lines to a CSV file and exit
  --check                Run an integrity check and exit
  --version              Print version information and exit
  --help                 Show help and exit

The database path also comes from POS_DB_PATH if it is set. A nightly backup is one line:

offline-pos --db /var/lib/pos/pos.db --backup "/backups/pos-$(date +%F).db"

--backup uses SQLite's online backup API, so it produces a consistent copy even while the till is being used — unlike cp, which can catch the file mid-write.

Where the data lives

Everything is in the file named by --db (default pos.db in the working directory). WAL mode creates pos.db-wal and pos.db-shm alongside it while the application runs; both are folded back into the main file on a clean exit.

To move a shop to another machine, copy the .db file. To inspect it, any SQLite tool will open it:

sqlite3 pos.db "SELECT receipt_no, sold_at, total_cents FROM sales ORDER BY id DESC LIMIT 5;"

The schema is documented in docs/DATABASE.md.

Project layout

include/pos/     Public headers, one per concern
src/             Implementation
  money.cpp        Integer-centavo formatting and parsing
  text.cpp         UTF-8-aware column padding, CSV escaping
  clock.cpp        Local-time stamps
  database.cpp     RAII SQLite wrapper, transactions, migrations
  *_repository.cpp All SQL, one repository per aggregate
  console.cpp      Line-based, EOF-safe terminal I/O
  app.cpp          Menus and screens
  main.cpp         Argument parsing and start-up
tests/           Test suite and its dependency-free harness
third_party/     The vendored SQLite amalgamation
docs/            Architecture, schema and user guide

The dependency direction is one-way: app uses repository uses database uses SQLite. Nothing below app prints anything or reads input, which is what makes the repositories testable without a terminal.

Testing

make test
# or
cmake -B build -DPOS_BUILD_TESTS=ON && cmake --build build && ctest --test-dir build --output-on-failure

64 tests cover money parsing and formatting, UTF-8 column padding, schema migration, transaction rollback, stock guards, tax rounding, receipt immutability and the reporting queries. The harness is a single header (tests/test_framework.hpp) rather than a fetched dependency, for the same reason SQLite is vendored: the tests must run on a machine that has never been online.

Documentation

Limitations

Stated plainly, so nobody is surprised at a counter:

  • Single register. Two processes can safely share one database file, but there is no multi-terminal sync, no user accounts and no permissions.
  • Cash only. No card, e-wallet or split tender.
  • No refunds or voids yet. A mistaken sale has to be corrected with a stock adjustment. The ledger records the correction, but there is no negative sale.
  • No BIR-accredited receipt formatting. The receipt is a plain text document, not an official Philippine sales invoice.
  • Local time, not UTC. Timestamps are stored in the machine's local time so that daily reports are a simple date comparison. A shop that changes timezone should export and re-import.

License

MIT — see LICENSE.

SQLite is in the public domain; the vendored amalgamation in third_party/ carries its own upstream notice.

About

A C++ terminal-based offline POS system designed to manage products, inventory, sales, and transactions without requiring an internet connection.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages