Problem Statement
Nigerian filling-station owners run their forecourts on a paper exercise book: the manager writes down the opening dip, each attendant's pump meter and cash, deliveries received, expenses paid, and the closing dip. The owner — usually off-site, often holding several branches — has no way to see yesterday across every branch, no way to know which till came up short, and no way to prove that a posted entry was not quietly rewritten after the fact. The most fraud-prone moment, a tanker offload, is recorded with the least rigour.
The constraint is deliberate: there are no hardware integrations and no POS / payment-rail linking. The system trusts the manager's entries. So the discipline cannot come from telemetry — it has to come from the mechanics of the ledger itself: posting, signing, audit trails, and an irreversible void idiom.
Proposed Solution
A back-office operations tool with three fixed-spirit roles — Owner, Manager, Attendant — built around a single spine: the day-book. Every other view is a projection onto it. Managers open and close shifts on a phone on the forecourt; the system computes litres dispensed and reconciles cash-declared against expected gross (litres × posted price) live, flagging each pump row as balanced, short, or over. Owners open a multi-branch morning roll-up that summarises yesterday's litres, gross, variance and wet stock, and generates a prioritised "things to do this morning" list from the day's flagged items.
Integrity is structural. A posted shift is final; removing one requires typing the literal word VOID, leaves the entry struck-through-but-visible, and writes an immutable audit record. Every state-changing action is logged with actor, timestamp, and before/after value.
Full Solution Details
- Account, branches & staff: OTP-verified owner sign-up, an onboarding wizard, per-branch tanks (one per product, with capacity and reorder thresholds) and pumps (idle / live / offline), per-branch posting rules, a weekly roster grid, and a staff directory carrying each person's cumulative variance track-record.
- The day & shifts: opening dips that carry forward yesterday's close, shift open with per-pump opening meters, live closing-meter reconciliation, note-on-variance, a double-ruled shift total, "post all balanced shifts at once," and end-of-day closing dip with computed wet-stock variance.
- Deliveries: a guided four-stage tanker offload (arrives → dip before → offload → dip after & sign), each step timestamped, with delivery variance against a configurable tolerance and a witness-signed waybill that updates the tank balance.
- Pricing: price changes with effective date/time, required reason, and a pinning rule so a litre sold at the old price always reconciles to the old price; full price history per product.
- Expenses, voids, roll-up, reporting, tank readouts & charts, notes/@mentions, and a full audit timeline round out the modules.
Technical Documentation
Monorepo (Nx 22 + pnpm 9 workspace). One process per apps/ directory; shared, never-deployed code in packages/. A strict dependency graph is enforced: core depends on nothing, api depends only on core, ui depends only on core (no ui → api edge), and apps never import other apps.
apps/main-backend— Express public HTTP API. Feature modules each expose a singleregister(app)that mounts one Router. Services return result objects; controllers unwrap them and respond only through aResponseUtilenvelope ({ data, meta }/{ error: { code, message, field_errors } }) — neverres.json()directly. Zod validates every request body. Pagination is cursor-based, never offset.apps/web— React 19 + Vite 6 operator app, organised by Feature-Sliced Design. All server state flows through TanStack React Query (no bareuseEffect + fetch). Routes come from a sharedROUTEStable, endpoints from a sharedEPtable, icons from an@iconsproxy, classes through acn()(clsx + tailwind-merge) helper.apps/website— Next.js marketing site.packages/core— pure TypeScript: route table, domain types, the RBAC permission catalogue, kobo money helpers, time/format helpers.packages/api—kyHTTP client (with a token-refresh afterResponse hook) plus per-feature React Query hooks.packages/ui— ~43 exported React + Tailwind design-system primitives, previewed live at/preview.
Money is stored and computed in integer kobo (bigint) end-to-end; every money field is suffixed _kobo and only formatted to ₦ at the edge. The MongoDB layer is fully isolated — a single MongoClient is created in one file and only *.repo.mongo.ts files import the driver; multi-document writes (post-shift-plus-audit, sign-delivery-plus-update-tank) run through a withTransaction unit of work that degrades gracefully to session-free execution on a standalone (non-replica-set) Mongo.
Tech Stack
- Language: TypeScript 5 (strict everywhere —
noUncheckedIndexedAccess,exactOptionalPropertyTypes;anybanned) - Frontend (operator app): React 19, Vite 6, React Router 6, TanStack React Query 5, Tailwind CSS 3
- Marketing site: Next.js 15, React 19, Tailwind
- Backend: Node 20+, Express 4, MongoDB 7 driver, Zod, JWT (
jsonwebtoken) + bcryptjs, Helmet, Pino logging, ULID ids - Shared client: ky
- Tooling: Nx 22 monorepo, pnpm 9 workspaces, ESLint flat config, Prettier, Vitest + Supertest
System Design
pnpm + Nx monorepo
apps/website (Next.js 15) apps/web (React 19 + Vite)
marketing site operator app (Owner / Manager / Attendant)
| |
| TanStack React Query
| |
| packages/api (ky client + hooks, EP)
| | HTTPS (JWT access/refresh)
+--------------------+ v
| apps/main-backend (Express 4)
| register(app) feature routers
| auth . branches . staff . roles . shifts
| deliveries . pricing . expenses . rollup
| notes . audit . notifications . refs
| | | |
| Zod validate authorize(P.*) ResponseUtil
| | | |
| v v v
| services -> withTransaction (unit of work)
| |
| *.repo.mongo.ts (only place mongodb is imported)
| v
| MongoDB 7 (money in kobo / bigint; immutable audit log)
|
packages/core (pure TS: ROUTES, domain types, RBAC catalogue P.*, kobo money)
^ imported by every layer (core depends on nothing)
packages/ui (React + Tailwind design system, /preview) -> depends on core only
Smart Architectural Decisions
- Money as integer kobo (bigint) end-to-end. Reconciliation (
expectedGross = round(litres × pricePerLitreKobo),variance = expectedGross − cashDeclared) is pure integer maths with no float drift, and the formula lives in one tested, dependency-free function. - A single layered dependency rule, machine-enforceable.
core → nothing,api → core,ui → core, apps never cross-import. The domain model (routes, types, RBAC keys) is the one thing every surface shares, so backend authorization and frontend UI-gating key off the same permission strings. - RBAC as a frozen permission catalogue, not hardcoded role checks. The MVP's three roles are seeded as editable permission sets (
Owner / Manager / Attendant) over aP.*catalogue, so the product can grow custom roles without rewriting authorization — and the same keys gate the UI. - MongoDB isolated behind repositories + an opaque transaction handle. Only
*.repo.mongo.tsfiles touch the driver; services receive an opaqueTx(deliberately not typed as a Mongo session) so the storage engine could be swapped to SQL without touching business logic. Transactions degrade to session-free on standalone Mongo so dev/CI work without a replica set. - Backend-enriched ID references. List endpoints that carry raw IDs also return a
refsmap ({ id: { type, label, href_kind } }), so audit logs and tables render human, clickable labels from one server-side resolve instead of N client lookups. - Token-refresh as a transparent client hook. The
kyclient retries a single 401 through a shared, de-duplicated refresh promise, keeping auth invisible to feature code.
Impacts
Delivers an audit-grade operations ledger for a cash-heavy, low-connectivity, fraud-exposed business without any hardware or payment integration — trust is engineered into the data model (immutable audit log, struck-through-not-deleted voids, price-pinning so old-price litres always reconcile to the old price). The strict layered monorepo and shared domain core mean the operator app, marketing site, and API stay consistent by construction, and the kobo-only money discipline removes an entire class of reconciliation bugs.
Demonstrated Skills
- Domain-driven monorepo architecture (Nx + pnpm) with an enforced dependency graph and a shared pure-TS core
- Designing for integrity over telemetry: posting/signing semantics, immutable audit trails, irreversible void idiom, price-effectivity pinning
- Backend engineering: Express feature modules, Zod validation, a strict response envelope, cursor pagination, JWT access/refresh, MongoDB behind repositories with a portable transaction abstraction
- Permission-catalogue RBAC shared between server authorization and client UI-gating
- Modern React 19 data architecture (TanStack Query, Feature-Sliced Design, a 40+ component Tailwind design system)
- Financial correctness discipline (integer kobo / bigint money) and product-grade attention to a real, messy operational domain