- Screens
- 20
- Security policies
- 39
- Database tables
- 14
What it had to get right
This is software that quotes money and then holds a business to it. A figure on a screen is a promise somebody has to keep, so the engineering brief was narrow and strict:
- The number shown and the number charged can never disagree. Not after a page has sat open for an hour, not after the catalog changes underneath it, not with two people ordering at the same moment.
- An account reaches its own records and nothing else. Not because the interface hides the rest, but because the layer below refuses to hand it over.
- Today's edits cannot rewrite last month's history.
- Two languages all the way down, including error messages and printed documents.
- Phone first. The people who use it work from phones, not desks.
Everything below follows from those five lines.
Architecture
One TypeScript codebase end to end: Next.js App Router for the pages and the server actions behind them, React and Tailwind on the surface, PostgreSQL underneath through Supabase.
The boundary that matters is not between front end and back end. It is between what the browser is allowed to assert and what the server decides. The browser sends only intent: which account, which delivery day, which products and how many. Prices, totals, tax and whether the order is allowed at all are resolved on the server, read fresh from the database, inside a single transaction at the moment the order is accepted. Nothing the page says about money is taken at face value.
Access follows the same rule. Whether a record can be read is settled in the data layer and covered by tests, not left to each new screen to remember.
Key decisions
- The screen never decides the price. A page someone opened an hour ago is out of date, so every order is priced again from scratch when it is submitted. If that disagrees with what was on screen, the order stops instead of going through at the wrong number.
- Order lines are snapshots, not lookups. Each line keeps the figure it was sold at, so changing a number today can never quietly rewrite paperwork that was already issued.
- Absence is the default. Only a rule that differs from the standard is stored, so a new account needs no setup at all, and undoing a rule is a deletion rather than a number somebody has to remember and retype.
- Confidential figures never reach the browser. They are not hidden with CSS or filtered in the client, where anyone can read the page source. The endpoints that would return them answer only to the role that owns them, so there is nothing in the delivered page to find.
- A refusal is a sentence. When the server rejects something it comes back as a readable line in the reader's own language, not a status code, a stack trace or a blank screen.
- Ten outside dependencies in the whole thing, where a project this size usually collects dozens. Fewer moving parts means fewer places for a rule about money to be quietly misread, and less to go stale in a year.
- A missing translation fails the build. 875 phrases across the two languages, and the build stops rather than shipping a gap onto somebody's screen.
Engineering practice
340 automated checks run against a real copy of the data. They assert the things that are expensive to learn about late: that tax adds up, that one account cannot read another's records, that totals still balance to the last togrog, and that a stale price is refused instead of honoured.
89 database migrations, each one guarded. Every change checks the ground before it moves, so a deploy that meets a state it did not expect stops rather than half applying.
Layouts are measured, not eyeballed. Mongolian runs about twice as wide as the same sentence in English, so every layout change is rendered at phone width and measured, with a specific check for whether any amount of money is being cut off. A clipped figure reads as a different number.
The application and its data sit in the same region. Both are hosted in Seoul, the closest to Ulaanbaatar, so a request does not cross the Pacific and back before the page can answer.
Outcome
About 64,000 lines of code, live on the company's own domain and in daily use. It installs on a phone like a native app, which is how most people open it.
I built and shipped all of it alone: requirements, data model, design, both languages, the launch, and the support since.
