Farm Warehouse
Field-operations platform for productive-use equipment financing across rural Nigeria

Overview
Farm Warehouse Nigeria Limited finances the equipment that earns people a living, mills, grinders, threshers and freezers, to farmers and processors in rural Nigeria, on lease-to-own terms so they can pay for a machine while using it. Delivering that is really six businesses stitched together. You have to qualify a community and stand up a hub inside it. Register the people who will use the equipment and verify who they are. Keep a catalogue in which every machine variant carries its own price and its own repayment terms. Train someone before trusting them with a machine. Work out whether a household can genuinely afford the repayments. Then follow every individual unit from the warehouse, to the hub, to the farm/residence it ends up on. There was already software in place, built as a marketplace with an admin panel added for operations. It had aged badly. The business had moved on from it, parts of it no longer matched how anyone actually works, and it had become expensive to change. Much of the operational half never made it into software at all, and still ran on paper forms, spreadsheets, Google Drive and WhatsApp. This platform is the purpose-built replacement for that half and more, designed from the start around field agents on phones in places where the signal often is not there at all. It is deployed and working behind login, and field rollout is scheduled to start soon.
My role
I am the IT Officer and Systems Administrator at Farm Warehouse, and this platform is part of the work I was hired to do, amongst other IT functions. Fullstack developer, responsible for driving most of the build. That has meant most of what is described here: the data model, every screen, the API layer, the permission and data-visibility system, the offline stack, the design system, the verification harness and the QA rounds that hardened it.
The problem
Farm Warehouse already had software, built as a marketplace with a back-office admin panel bolted on for orders, products, loans and repayments. It was still in use, but it had drifted a long way from the business it was meant to serve. Several things had gone wrong at once. How the company operates had changed considerably since the system was designed, so whole areas of it no longer matched anything anyone did. The codebase was cumbersome and had never been architected for what it grew into, which made every change expensive and left features breaking. And because the whole thing had been conceived from a marketplace point of view, large parts of it were simply irrelevant to daily operations. The operational half of the business is a different shape entirely. Qualifying a community, standing up a hub inside it, registering the people who will use the equipment, training them, working out whether a household can afford the repayments, and following individual machines from a warehouse to a farm: none of that is a shopping cart with an admin panel on top. So much of that work had never made it into software at all. It ran on paper, spreadsheets, Google Drive and WhatsApp. The documents deciding who received financing were Excel sheet trackers. The community survey was via KoboCollect (and other ODK apps). Customer records lived in a Drive folder, and coordination happened in chat and email threads. Two further things made replacing it harder than a normal internal build. The people entering the data are field agents standing in rural communities holding phones, frequently with no signal at all. Any system assuming a constant working connection would simply not have been used.
The solution
One system rather than a suite of them, built module by module against a traced list of requirements rather than a set of wireframes, so what shipped could always be checked against what was asked for. Three decisions shaped everything that followed. Signing in is handled by a proven identity service, while everything that service does not model, the eighteen roles, the account states, the lockout rules, and above all the question of which records each person is allowed to see, lives in the application where it can be reviewed and tested. Anything needed in more than one place was built once and reused. A single engine drives every workflow that moves something from one state to the next, so a screen can only ever offer the buttons the server will actually accept. And every write a field agent makes is queued on their own device and replayed when the connection returns, with the server refusing to accept the same submission twice.
Key features
- Take a community from first visit to a working hub: the assessment, the survey scoring, the local leadership committee, the awareness sessions, and two points where a manager must approve before anything continues.
- Register the people who will use the equipment, upload their identity documents to private storage, and have their verification status recalculated automatically, including a daily check that catches documents as they expire.
- Keep a full equipment catalogue where every variant carries its own price, dated so past agreements are never rewritten, with room to negotiate a specific price for one community or one beneficiary.
- Run an application from first interest through training, credit assessment and identity checks to a signed contract and an invoice, watching the whole pipeline on a board, with a daily sweep that flags anything stalling.
- Follow every individual machine by serial number, from warehouse to hub to the location it ends up on, with a movement history that still reads correctly years later even after a warehouse has been closed.
- Work with no internet at all. Field staff can sign in offline, complete forms, attach photos, and have everything upload itself when signal returns, on a shared tablet without one agent's work being filed under another's name.
- Bring staff on by invitation only, with temporary passwords they must replace, automatic lockout after repeated failed logins, and a permanent record of who changed what and why.
Architecture & engineering
The whole thing is a single Next.js 16 deployment with its functions pinned to Frankfurt, talking to Supabase Postgres through the transaction-mode pooler. Those two changes together removed about a hundred milliseconds of transatlantic latency from every query and cleared the intermittent connection failures, taking concurrent-connection success from 13 out of 30 to 30 out of 30. Identity is deliberately decoupled from the authentication provider. A person's profile row carries its own id with a nullable link to an auth user, so someone can exist as invited, or as a farmer holding temporary credentials, before any login account has been created for them. Permissions are defined in code rather than stored as rows, across twenty modules, which means a role's grants are reviewable in a diff instead of being something you have to query the database to understand. Data visibility is composed per module rather than through one shared helper. That was deliberate: the beneficiary scope has no hub relationship and the application scope must never touch community, so a single shared predicate would have been wrong for at least two modules. Every list, detail and count query builds its own. Row-level security is on for all tables, with a deny-all posture on the five holding dates of birth, dwelling coordinates, income and national identity scans. Migrations are hand-written and applied one at a time inside a single transaction, through a script built for the purpose after the stock tooling proved unsafe against a live database with no staging copy beside it.
The hardest problem
The one worth telling involves a Postgres array comparison that cannot work with this driver, used in eight separate raw-SQL visibility predicates. Because every scope check short-circuits to true for a user who can see everything, an administrator saw perfect pages while every hub, community and region scoped user got a blank shell behind a successful HTTP response. It was deployed that way, with the unit tests, the smoke scripts and the SQL probes all green over the top of it. The fix is a single helper whose test pins the SQL it emits, and that detail matters: the broken form also returned a perfectly valid SQL object, so a test asserting that it returned one would have passed on the bug. A second defect had the same shape. Recording one training attendee's outcome terminally closed the other nineteen enrolled applications, and it stayed invisible because the fixture session used by the smoke test had only a single attendee in it. Then the harness itself came under suspicion. Several assertions turned out to be passing for the wrong reason, and one probe reported a false failure against production code that was correct: a form value filled before React has hydrated poisons React's value tracker, so every identical retry after that is a silent no-op.
Results
Fourteen modules are deployed and working behind login, with a series of demos to stakeholders, with each demo shaping what came next. Field rollout has not started. The system is being validated round by round before real beneficiary data goes into it, so what follows is engineering evidence rather than usage. Some of the business is still ahead of this platform. Loans, repayments, transactions and maintenance are specified but not yet built here, so the older marketplace system keeps running alongside while the operational side moves across. What the QA rounds caught before rollout matters more than the module count. Recording one training attendee's result would have permanently closed the applications of the other nineteen people sitting in the room, while the confirmation message read as though only one had been touched. And the credit-scoring formula in the specification understated a household's monthly repayment by roughly twenty-one times, driving most of the score that decides who gets financed: it would have approved families who could not possibly have paid. That one was raised with the business as a specification defect rather than quietly corrected in code.
Key learnings
Two rules came out of a single defect and now govern everything I verify. A successful response is not a rendered page. The server answered every request correctly while showing scoped users a blank shell, and hundreds of passing tests never noticed, because they asserted on the status code rather than on the body. And an administrator is not a test subject. Someone who can see everything only ever exercises the line that grants them everything. The users who expose the bug are the ones whose access is limited, and a scoped user who can see nothing will pass every negative assertion for entirely the wrong reason. The wider lesson was about what actually escapes. In nearly every case here the missing test was not missing. It existed, it passed, and its setup was simply too simple to contain the situation that broke.
Curious how this works?
This one's a private build; request a walkthrough and I'll show you around.