PlotWise
An internal inventory platform for agricultural R&D field operations, built on an immutable transaction ledger. Live across 8 sites in two countries.
PlotWise is built on one rule: never edit a balance. Stock, value, expiry, and usage all derive from an immutable transaction log, so every number traces back to who moved what, and when. It replaces the disconnected spreadsheets that field operations ran on, used daily by operators working on their feet, not at a desk.
The whole interface is built on the ledger, not on editable fields. You never type a new quantity; you record a movement (receive, use, check out, or adjust) and the balance is derived. The history stays intact and auditable, and a mistake is corrected with another transaction rather than a silent overwrite.
The primary field actions live in a persistent bottom bar sized for a thumb: home, scan, receive, checkout, usage. For someone entering twenty records between rows of plants, the common path stays one tap away instead of buried in a menu.
Scanning is tuned for the field, not the warehouse. The scanner looks up a container's own label before falling back to a product barcode, because in trial operations the thing in your hand is a specific container, and that is what the user actually needs to find.
A Postgres ledger sits at the core: immutable transaction rows, balances derived through views like an on-hand rollup rather than stored, and a schema of items, categories, locations, lots, checkouts, and import tracking around them. Row-level security enforces per-site and per-region access at the database, through a can-access-site check, so the rules hold no matter which client is talking to it. Server components read through request-scoped Supabase clients while the interactive forms write transactions directly, and the same access model is mirrored in middleware.
Shipped and in daily use across the operation. It replaced the spreadsheet sprawl and surfaced inventory that had never been tracked in one place before. The working surface runs the full loop: role-aware onboarding and site selection, inventory listing, receiving, usage and checkout, a barcode scanner, activity history with export, notifications, and analytics, all installable as a PWA.
Derive every balance, never store one
- Chose
- An immutable transaction log, with stock, value, expiry and usage all derived from it through views like an on-hand rollup.
- Over
- A stored balance column that each flow updates in place, the way the spreadsheets it replaced worked.
- Because
- Every number then traces back to who moved what, and when, and no flow can quietly overwrite a quantity.
- Cost
- Nothing can be quietly fixed. A wrong entry is corrected by posting a second, compensating transaction, and both rows stay in the history permanently; there is no edit, and no way to make a bad number simply go away.
Enforce access in Postgres, not in the app
- Chose
- Row-level security doing per-site and per-region checks in the database through a can-access-site predicate.
- Over
- Scoping reads and writes in the application layer, where the queries are already being written.
- Because
- The rules then hold no matter which client is talking to the database.
- Cost
- The same access model has to be mirrored outside the database anyway, in middleware and in the request-scoped server clients, so one rule is now maintained in more than one place and they can fall out of step.
Scan the container, not the product
- Chose
- A scanner that resolves a container’s own label first and only then falls back to a product barcode.
- Over
- The warehouse convention of treating the product barcode as the identity of the thing being scanned.
- Because
- In trial operations the thing in your hand is a specific container, and that is what the operator actually needs to find.
- Cost
- Two lookup paths instead of one, with a fallback that has to be kept correct even though the common case never reaches it.
The append-only guarantee was earned, not designed in: early migrations still allowed updating and deleting transactions, and it took a later one to drop those policies and truly lock the ledger. Starting again, I'd commit to immutability from the first migration. There is also real cleanup waiting: the scanner and a number of catch blocks lean on any in an otherwise strict codebase, a couple of forms read the current site two different ways, and the PWA manifest and README still carry stale copy from an earlier iteration. The ledger-first model itself, deriving every balance instead of storing it, is the decision I'd keep without question.