Case study

GreenEarth Agro Industries

Eight modules replacing a folder of spreadsheets, so executives overseas can see an agro-processing operation in Ajumako as it actually runs.


Record

Client
GreenEarth Agro Industries Limited
Work
Design and build, front to back
Period
August 2025 to present
Stack
Next.js App Router, NestJS, GraphQL (code-first), PostgreSQL, TypeORM, Tailwind v4
Availability
Runs on site. No public URL.

The problem

The operation ran on Excel. Stock sat in one workbook, procurement in another, farm activity in a third, and they disagreed with each other. Reconciling them was somebody's job every week, and the reconciliation was itself a spreadsheet.

The people who needed the numbers most were not in Ghana. What reached them had been summarised twice and was several days old, which meant every decision about buying, pricing, or production was made against a picture of the factory that no longer existed.

The failure was not that the spreadsheets were wrong. It was that nobody could say which one was right.

Plate 01

The procurement report, showing six months of seed spend and quantity as paired bars with a weighted average unit price line, above a per-batch receipts table.The procurement report, showing six months of seed spend and quantity as paired bars with a weighted average unit price line, above a per-batch receipts table.

What was built

Eight modules, one record

  • Inventory: live stock, batch and lot tracking, minimum-stock alerts, storage locations.

  • Production control: run management for oil and pallet lines, input to output mapping, cost per run.

  • Procurement and sales: supplier and buyer records, purchase orders, sales orders, payment tracking.

  • Farm operations: crop monitoring, activity logs, worker attendance, input costs.

  • Workforce: attendance, payroll, salary disbursement, shift scheduling.

  • Finance: cost of production, profit and loss, two currencies (GHS and USD).

  • Executive reporting: daily, weekly and monthly KPI dashboards, exportable.

  • Communication: automated SMS and email notifications, task management.

What shaped the build

The network is not a given. Field and farm work happens where connectivity drops without warning, so capture writes to IndexedDB first and synchronises in the background. A lost signal costs nobody their morning's entries.

The decision-makers are elsewhere. Role-based access and per-role dashboards exist because a CEO abroad, a factory manager and a field worker need different slices of the same records. Every state change is written to an audit log, which is what makes a remote approval defensible later.

A batch has to stay identifiable. Goods received carry batch identity from the receiving register through production into finished output. In agro-processing that is not bookkeeping, it is the ability to answer where a bad lot came from.

It runs on site, not on the internet. The system is deployed locally at Ajumako with a hosted database. For a single site on unreliable public bandwidth that is the correct arrangement: the factory keeps working when the connection does not, and the executives abroad still read from the same database. It is also why there is no link on this page to click.

Plate 02

The system dashboard, showing role preview tabs for leadership, operations, commercial and finance alongside daily operational figures.The system dashboard, showing role preview tabs for leadership, operations, commercial and finance alongside daily operational figures.

Plate 03

The production workspace, showing active lines, open work-in-progress lots and remaining output lines.The production workspace, showing active lines, open work-in-progress lots and remaining output lines.

Outcome

Measured outcomes
MeasureBeforeAfter
Operational data discrepanciesBaselineDown 80%
Manual reporting effort~20 hrs / weekRecovered
Source of truth for stock3+ workbooks1 system
Executive view of the factoryDays oldLive
Staff working in the system050+

In hindsight

The entities carry both their database mapping and their GraphQL type on the same class. It ships, and it was fast to write, but it means the shape of a table and the shape of the public API are the same decision. Renaming a column is an API change whether you meant it to be or not.

On the next system I separated them: persistence models on one side, API types on the other, with a deliberate mapping between. It costs a file per module and buys the freedom to change either side alone.

Have an operation that runs on spreadsheets?

The first conversation is free and usually short.

Start a project