Loading portfolio
All Work

PlotWise

An internal inventory platform for agricultural R&D field operations, built on an immutable transaction ledger. Live across 8 sites in two countries.

Design + Build · Solo
Sites
0
Countries
0
Languages
EN · ES
Field actions
0
02(Context)

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.

03(Design)

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.

04(Engineering)

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.

Input
Receive · scan · checkout
Validate, then write a movement
→
→
Read model
Views + aggregates
On-hand, value, thresholds derived
stack   Next.js · TypeScript · Supabase · PostgreSQL · Tailwind · shadcn/ui · next-intl
05(Why end-to-end)
Because I owned both the inventory interaction model and the database write model, I could build every flow on one immutable ledger, so receive, usage, checkout, and scan all write the same kind of signed transaction. The alternative was the one every inventory tool starts with, a stock column each flow updates in place: quicker to build, and four write paths that have to agree with each other forever.
The case for one person, end to end
06(Outcome)

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.

0
Transaction types
0
Editable balance fields
07(Key decisions)

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.
08(What I'd do differently)

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.

Next project
Resolv ↗
Product design · Frontend / Next.js · Supabase
← Back to selected work