System · 2025 · shipped
A B2B order management system where several companies operate inside one deployment without ever seeing each other's data. Product catalogues, client accounts and purchase orders are scoped to a tenant at the query layer, and an order can only move through the states the business actually permits.
A single-tenant order tool is straightforward. The moment more than one company shares a deployment, every query becomes a place where one customer can read another's data — and a leak here isn't a bug report, it's the end of the product.
The second problem is the order itself. An order is not a row that gets edited; it is a thing that moves through states, and only some moves are legal. Left unconstrained, an order can be fulfilled before it is approved, or rejected after it has shipped.
Tenancy is enforced at the data-access boundary rather than in the templates. Every query is scoped by company_id, so isolation is a property of how data is fetched, not of whether a given page remembered to filter. Forgetting the scope produces no rows rather than someone else's rows.
Authorisation is expressed as decorators — admin_required and client_required — so the permission for a route sits directly above the route, where it can be read and audited without tracing calls.
The order lifecycle is a state machine with explicit legal transitions. Illegal transitions are rejected server-side, not hidden in the UI.
Currency is stored as integers in paise. Floating-point money produces totals that are almost right, which in a purchasing system is worse than being obviously wrong.
SQLite, not Postgres. For a single-node deployment at this scale SQLite removes an entire operational surface, and the schema is portable when concurrent write volume justifies the move. That threshold is a real number, not a someday.
Server-rendered Jinja, not an API plus a SPA. The system has no second client, so a JSON API would have been an interface built for an imaginary consumer. Adding one later is a smaller job than maintaining one that nothing calls.
Row-scoped tenancy, not a database per tenant. Simpler to operate and adequate at this size; schema-per-tenant is the next step if a customer ever requires physical separation.
The isolation guarantee is currently upheld by discipline. It should be upheld by tests — a suite that asserts, for each model, that a request authenticated as tenant A cannot reach a record belonging to tenant B.
State transitions belong in one table-driven definition rather than being checked at each call site, so that adding a state is a single edit.