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!
========================================================================
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. |
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_checkandVACUUMfrom 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
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.
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 cleanOn 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.
- 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/.
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.
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.
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.
make test
# or
cmake -B build -DPOS_BUILD_TESTS=ON && cmake --build build && ctest --test-dir build --output-on-failure64 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.
- docs/USER_GUIDE.md — running the till, day to day
- docs/ARCHITECTURE.md — how the layers fit together
- docs/DATABASE.md — schema, constraints and migrations
- CONTRIBUTING.md — building, style and pull requests
- CHANGELOG.md — release history
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.
MIT — see LICENSE.
SQLite is in the public domain; the vendored amalgamation in third_party/
carries its own upstream notice.