live in productionInventory & POS System· 2025—2026

Stock Management

Full inventory and point-of-sale system for a retail client — receiving, transfers, sales, returns, and printed invoices with a workflow shaped around what actually happens at the counter. Separate frontend (Vite + React 19) and backend repos, both deployed.

ViteReact 19TanStack QueryTanStack TableZustandreact-pdfTailwind v4Better-Auth

The brief

A retail client was running their inventory in three spreadsheets and a Telegram group. Stock counts disagreed across them weekly. Customers got the wrong invoice. Receiving and selling were both manual data entry into the same file. They asked for “a simple inventory app.”

What they actually needed was a tool that mirrored how stock physically moves through their shop — receiving, transferring, selling, returning, printing — without making someone type the same SKU twice.

The problem

The status quo:

  • Two systems of record — physical stock on the shelf and a spreadsheet that's always one delivery behind.
  • Invoicing as an afterthought — printed from a different tool, manually keyed in, often wrong.
  • No transfer concept — when stock moves between locations it just “disappears” and reappears.
  • No audit trail — when a count is wrong, no one can tell when it went wrong.

The approach

I shipped this as two separate repos — a Vite + React 19 frontend and a TypeScript backend — both deployed on Vercel. That split was deliberate: the frontend is the high-iteration surface (the counter staff use it daily), and the backend is the slower-changing source of truth.

The data model treats every stock change as an immutable movement event — receiving, sale, transfer, adjustment — and the current count is derived from the sum. Nothing “updates” a stock level directly. That single decision makes the audit trail free and the bugs visible.

MOVEMENT LOG+ 100 receiving · 2026-05-22− 3 sale · 2026-05-23− 10 transfer · 2026-05-24− 1 sale · 2026-05-25+ 1 return · 2026-05-25SUM()DERIVED COUNT87units on hand · SKU-1042no row in any table directly stores "87"
fig. — movements are the truth; current counts are derived

The hard parts

Printing invoices that look right

Retail invoices are a layout problem disguised as a data problem. Headers, line items, totals, footer notes, signature block — and it has to print to whatever cheap thermal or A4 printer is on the counter. I render them in-browser with react-pdf, preview them in a react-to-print flow, and let the user save or print. Layout lives in code, not in a template the client has to maintain.

Optimistic UI with a real audit trail

The counter staff don't want to wait for a server round-trip every time they ring up a sale. TanStack Query's optimistic mutations let the UI update instantly while the movement event posts in the background. If the server rejects (price mismatch, out of stock), the optimistic state rolls back and a toast explains why. The movement log records both attempts, so an audit shows what happened.

The old way was “type, type, hope.” Now I scan, I see the number drop, I move on.

counter staff, two weeks in

State that survives a refresh

Carts, draft transfers, draft returns — anything mid-flight — lives in Zustand with localStorage persistence. A staff member can start a sale, get pulled away to help a customer, come back twenty minutes later, and find the cart exactly as they left it. Small, but it's the kind of detail that determines whether software gets used.

Vite + React 19shell
Fast HMR, fast page load on the counter PC.
TanStack Querydata
Server state + optimistic mutations.
TanStack Tableui
Dense, sortable tables for stock + sales lists.
Zustandstate
Persisted local state for in-flight carts and drafts.
react-pdf + react-to-printprint
Layout-driven invoices, no templates to maintain.
Better-Authauth
Role-scoped sessions: cashier vs admin.

What's running today

2repos · frontend + backend
audit history · derived from events
0manual stock reconciliations · placeholder
A4 / 80mminvoice formats supported

Both the frontend and the backend are deployed and in daily use at the client's counter.

What I'd do differently

The two-repo split was right for this team, but it added enough deploy-coordination friction that next time I'd reach for a monorepo from day one — Turbo's pipelines handle the “don't ship the frontend if the backend types changed” case more elegantly than a careful git habit.