Multi-branch, multi-business inventory, POS and back-office platform for the Nigerian retail and wholesale market. Built for appliance and gadget shops, furniture showrooms, wholesale and general merchandise dealers, and building materials and hardware yards — from the same codebase, configured per business.
StockRidge is an offline-first PWA: an SQLite database on the server or in the browser's device storage, and Cloudflare D1 online. A branch keeps selling when the network drops, and reconciles when it returns.
Live sample: https://sample.stockridge.workers.dev — a clean deployment with
one administrator and nothing else, so you can create a business and watch
provisioning build it. Production: stockridge.stockridge.workers.dev.
| Area | Detail |
|---|---|
| Trading | POS with barcode and variant picking, wholesale price tiers, customer classes, promotions, vouchers, discounts with authority limits |
| Stock | Batches with landed cost, transfers between branches, stocktakes that freeze the system quantity, adjustments with approval, reorder alerts, serial numbers |
| Multi-branch / multi-business | One owner can hold several businesses and several branches; every row is scoped by branch_id, which is the single source of scoping truth |
| Nigeria-specific | VAT 7.5% extracted from inclusive prices, the 2024 Withholding Regulations held as editable data, West Africa Time bucketing throughout, geofenced attendance, NUBAN validation |
| Credit & instalments | Debtor ledger, credit limits, Ajo and instalment plans with schedules and part-payments, layaway with item holds |
| After sales | Serial-tracked warranty claims, product recalls, delivery jobs with vehicles and zones, installation jobs |
| Money | Cashbook per branch, till sessions with variance reasons, branch safe with deposits and banking, expenses with approval, double-entry general ledger |
| Control | Hash-chained append-only registers, audit log, idempotency keys on every write, LWW sync with conflict capture, login throttling, one open till/stocktake/plan per branch |
The unusual part of this codebase, and the reason it stays correct:
server/routes/* ← the only implementation of any endpoint
│
server/lib/http.js ─┴─ createApp() + buildRoutes()
┌──────────────┴───────────────┐
server/app.js worker/src/index.mjs
Node + better-sqlite3 Cloudflare Worker + D1
(a shop's own server) (stockridge.stockridge.workers.dev)
buildRoutes() registers 166 routes against an abstract storage interface.
Node satisfies it with better-sqlite3; the Worker satisfies it with
worker/src/d1.js. There is no second implementation of any route, service,
query or business rule — no drift, because there is nothing to drift from.
The same two .sql files in schema/migrations/ are applied verbatim by both
runtimes: 76 tables, 22 views.
Chosen when the client creates their business; each brings its own categories, units of measure, compliance requirements (SONCAP/SON) and warranty rules, plus a starter catalogue and chart of accounts.
| Code | Vertical |
|---|---|
ELECTRONICS |
Electronics, Appliances & Gadgets |
FURNITURE |
Furniture & Home Furnishings |
WHOLESALE_RETAIL |
Wholesale & Retail General Merchandise |
BUILDING_MATERIALS |
Building Materials & Hardware |
GENERAL_RETAIL |
General Retail (fully configurable) |
Requires Node 20.11 or newer.
npm install
npm run db:migrate # applies schema/migrations/*.sql
npm run db:seed # OPTIONAL: a demo business, for development only
npm start # http://localhost:8787npm run db:seed builds a full demo business with sample sales. It is for
development. A client deployment is handed over with one administrator and
nothing else, and the client's own provisioning flow creates the rest.
npm test # 260 tests: unit, integration, end-to-end
npm run verify # the three static audits, then the tests
npm run db:audit # placeholder/value mismatches, arbitrary-row reads
npm run names:audit # undefined or unimported names
npm run args:audit # service functions called with keys they ignoreRequires Node 22 or newer (wrangler 4 refuses older) and a Cloudflare API
token in .env.deploy — see .env.deploy.example and copy it.
node tools/deploy-cloudflare.js # production (the default config)
node tools/deploy-cloudflare.js --env=sample # → sample.stockridge.workers.devOne command, seven steps, idempotent: verify the token and resolve the account →
create the D1 database if it does not exist → write its id into
worker/wrangler.toml → apply the migrations → seed one administrator →
set the JWT_SECRET → deploy the Worker (which serves both the API and the PWA)
→ smoke-test the result, including a real sign-in.
Useful flags:
| Flag | Effect |
|---|---|
--dry-run |
Everything except the deploy itself |
--skip-seed |
Never touch the administrator row |
--reset-pin |
Overwrite the administrator's PIN hash (see below) |
--pin=48213 |
Choose the PIN instead of generating one |
--env=sample |
Deploy an environment from worker/wrangler.toml to its own Worker and its own database |
A deployment starts with one administrator, one withholding schedule, and no business at all. That is deliberate: the client's first act is to describe their business, and provisioning builds the chart of accounts, product categories, customer classes, price lists and a starter catalogue for the vertical they choose.
sign in as the administrator
→ Businesses → Create a business → choose the vertical
→ Users → create an OWNER for that business
→ hand over the owner's PIN
GET /api/diagnose reports whether the deployment is correctly wired, and
GET /api/health/ready reports whether it is ready to trade. On a fresh
deployment readiness answers awaiting_first_business, which is correct and not
a fault.
The seed is INSERT OR IGNORE: re-running the deploy never resets a PIN the
client has already changed, and the deploy prints the existing PIN as unchanged.
To deliberately replace it:
node tools/deploy-cloudflare.js --reset-pin| docs/deployment.md | Fresh account → running deployment, and shipping a change afterwards |
| docs/d1-operations.md | Reading production, migrations, backups and restores |
| docs/storage-and-r2.md | Images, signatures, and what file storage would take |
| docs/ci-cd.md | The two workflows, and why the deploy one is manual |
| docs/client-handover.md | The first hour with a real business |
| docs/troubleshooting.md | Something is wrong and the shop is open |
| STATUS.md | What is built, what is proven, and what is not |
domain/ 15 pure modules — money, tax, time, credit, pricing, access,
validation. No database, no framework, no Node built-ins.
server/
lib/ storage interface, HTTP router, responses, audit, throttling
middleware/ authentication, scope resolution
services/ sales completion, general ledger, provisioning
routes/ 17 route modules — the whole API
app.js the Node entry point (static files, CORS, SPA fallback)
worker/
src/index.mjs the Cloudflare entry point
src/d1.js the D1 storage adapter
wrangler.toml bindings, migrations, cron, environments
schema/migrations/ applied verbatim by BOTH backends
public/ the PWA — 23 views, service worker, offline queue
test/ unit, integration and end-to-end suites
tools/ migrate, seed, deploy, and the three static audits
docs/ operational documentation (see above)
scripts/ push-github.sh — authenticated push without storing a token
- PINs are hashed with PBKDF2-SHA256, 100,000 iterations, per-PIN salt, constant
time comparison. 100,000 is a platform ceiling, not a preference — the
Cloudflare Workers WebCrypto implementation refuses anything above it, and a
hash written above it can only be verified on Node. See
domain/crypto.js. - Sessions are 12 hours (a shop day), one active session per user, revoked on sign-out and revalidated against the live user row on every request — so deactivating a user or moving their branch takes effect on their next request.
- Every store's scope comes from
branch_id, applied in the query, never by the client. - Report a problem per
SECURITY.md.
Proprietary. © StockRidge. All rights reserved.