← All work

Multi-tenant SaaS · POS & inventory

InFlowTrack

Point-of-sale and inventory for small Nigerian retailers that records what the bank and card terminal actually confirmed, not what staff say happened, and flags every discrepancy.

InFlowTrack: Point-of-sale and inventory for small Nigerian retailers that records what the bank and card terminal actually confirmed, not what staff say happened, and flags every discrepancy.

Built

19k lines of TypeScript on Cloudflare’s edge, a database per tenant, 586 tests and four payment providers

Grew

Wrote the positioning and the marketing site around one line: “I’ve sent it” is not payment confirmation

the result

Live in production, with real shops taking card and transfer payments

lines of TypeScript
19k
modules
108
automated tests
586
payment providers
4

The problem

Small multi-outlet retailers in Nigeria (building-materials yards, paint shops, pharmacies, provisions stores) lose money they can’t see. Sales settle in cash, bank transfer and card across two or three branches, and the owner is rarely there for most of them.

When a cashier says “the customer transferred”, the business has no independent record: the claim and the receipt are the same act. Stock walks out, prices get quietly changed and put back, and the gap only shows up months later as unexplained shortfall. Existing POS software records what staff type in. If a cashier logs a transfer that never arrived, the software agrees.

InFlowTrack’s bet is to reconcile three independent records (what was sold, what the payment provider actually confirmed, and what’s on the shelf) and make every disagreement visible, attributed to the person responsible.

How it’s built

Database per tenant, on the edge. Each business gets its own isolated Cloudflare D1 database, provisioned through a queue when its KYC is approved. One Worker dispatches by host: the apex is the marketing site, admin.* is the platform owner, and every other subdomain resolves to a tenant. Tenant databases are reached through a D1 REST adapter instead of static bindings, which cap at about 5,000; the REST path scales to the plan’s 50,000. A KV-cached subdomain lookup keeps the hot path fast.

Every request runs the same spine: resolve the subdomain, check the tenant’s status, validate the session against that tenant’s database, check membership and role, enforce branch scope, then handle. Cross-tenant and cross-branch access fail at the middleware layer.

Money is integer kobo everywhere, and no float ever touches currency. Quantities are integer thousandths, so one model sells 2.5 litres of paint and sealed tins alike.

Decisions worth calling out

Payment verification is a first-class idea. Every sale is stamped webhook_verified (a provider confirmed the money), self_reported_cash, or self_reported_override (a cashier says money arrived that nothing confirmed). Overrides are counted per cashier and flagged above a threshold: the accountability signal for exactly the fraud the product targets.

Card payments arrive from the terminal with no order reference. Matching by amount breaks when two customers owe the same figure, so confirmed payments become their own records (payer, bank, reference) that a person attaches to an order. The provider’s verification is kept, and a human supplies the link. Part-payments and overpayments are handled too.

Everything that moves money or permissions is audited. Price changes keep before and after; voids, refunds, write-offs, role grants, key changes, till closes and terminal registrations all go to an append-only log, shown in plain English a shopkeeper can read.

Business logic is pure functions over rows and repositories only fetch, which is why all 586 tests run without a live database. Security covers PBKDF2 with a pepper, encrypted provider secrets, mandatory 2FA for owners, HMAC-verified webhooks, and audited, auto-expiring impersonation for support.

Hard problems I solved

Webhook signatures failing in production. I built diagnostics that captured provider headers only on failures, which showed the real cause: a mismatched subscription secret between two Cloudflare accounts, not a code bug.

The D1 REST adapter returns no rows-affected count, so a concurrency guard that relied on it silently failed every payment pairing. I rewrote it to read the row back instead.

Migrations run separately from deploys, so new columns degrade gracefully instead of throwing a 500, and a half-migrated shop always falls back to the console screen that can fix it.

CSV catalogue import validates everything in a dry run first (decimal quantities, duplicate names) so a 300-item paint catalogue never half-imports.

Stack

TypeScript, Cloudflare Workers, Hono, D1 (SQLite), KV, R2, Durable Objects, Queues and Vitest, with Monnify, Paystack, Flutterwave and Moniepoint for payments, Resend for email and Turnstile for bot protection.

Where it is now

InFlowTrack is live in production at inflowtrack.com, with real tenants taking real card and transfer payments.

let's talk

Something to build, or something to grow?

Tell me a little about it and I’ll reply within a day. Prefer email? That works too.

hello@logstar.dev
What’s it about?