StockFlow
Stock, sales and balances for a team of sellers.
- Year
- 2026
- Status
- Personal project
- Stack
- TypeScriptNext.js 16React 19Tailwind 4shadcn/uiSupabasePostgreSQLDocker
The problem
Tracking sales, stock and outstanding balances for several sellers by hand falls apart quickly: the stock no longer matches reality, totals depend on who does the maths, and nobody knows exactly who owes what. I wanted an application where those numbers cannot drift apart — including when someone actively tries to make them drift.
The architecture
A Next.js application that only talks to its database from the server, backed by a Supabase instance. The browser receives nothing but HTML and server actions: no key, no database address, so nothing it could query on its own.
The decisions that matter
- 01
Authorisation lives in the database, not the interface
Every table carries its own access policies, every write function opens with a guard, and direct writes are revoked. The test I apply before adding any check: could the whole interface be deleted without opening a hole? If the answer is no, the check sits in the wrong place.
- 02
No business calculation in TypeScript
Totals, costs, margins, debts, stock checks: all computed in SQL. Amounts shown while typing are indicative only — the one that counts is recalculated by the database. Without that rule a seller could submit a total that suits them, and two implementations of the same calculation would drift apart anyway.
- 03
No access key in the browser
The environment variables carry no public prefix: they are read at runtime and never baked into the code sent to the client. Verified rather than assumed — the 36 scripts loaded by the page were inspected one by one, and the Docker image holds no secret.
- 04
An access rule that called itself
The policy protecting the profiles table calls a function that must read that very table. Read normally, the policy re-triggers inside itself and the query spins forever. The function is therefore declared to run with its owner's rights, letting it read the table without going back through the access check.