Back to Projects
GolangFiberGORMReactTypeScriptNext.jsPostgreSQLRedisRabbitMQDocker SwarmOpenAITextractReact NativeAWSNginx

Vantagepoint ERP

A multi-tenant, AI native ERP platform for Zimbabwean businesses. Point of sale, inventory, purchasing, invoicing, accounting, fixed assets, payroll and ZIMRA fiscalisation, plus mining, construction, fleet and manufacturing verticals, with every customer provisioned onto their own isolated application and database stack within minutes.

Vantagepoint ERP

Project Overview

Vantage Point is an ERP platform for Zimbabwean businesses, and it is the largest thing I have built. The tills, the stockroom, the invoices, the general ledger, the payroll and the tax compliance all live in one system, so a number gets typed once and everything downstream agrees with it. Every customer runs on their own isolated stack with their own database, provisioned automatically, and can be selling the same day they sign up.

The numbers, since they are the honest way to describe scale: the ERP backend is 2,394 Go files and roughly 609,000 lines, with another 450 test files and 105,000 lines of tests next to it. The dashboard is 1,080 TypeScript and TSX files, about 356,000 lines. Around it sit five more services, a React Native till app, a marketing site and 35 database migrations. I was a finance major before I wrote software for a living, which is the only reason I was willing to start something this size: I already knew what a trial balance had to do, so the hard part was only ever the engineering.

The services are named after Greek gods, because it was two in the morning and I needed a naming scheme. Olympus is the control plane: it owns the tenant registry, the provisioning state machine, a durable Postgres job queue, plans and billing. It never touches Docker. Pegasus is the worker that does, draining Olympus's queue and enacting each job against Docker Swarm through the mounted daemon socket, deploying a dedicated application and Postgres stack per tenant. Zeus is the admin dashboard, Poseidon is the per-customer one, Hermes is the WhatsApp gateway and Cyclopes renders PDFs. The two rules that keep provisioning honest are that Pegasus never opens an SSH connection and never touches a tenant database, and that it will not reload the nginx ingress until the new tenant's own `/readyz` endpoint says it is alive. A tenant that appears in DNS before it can answer a request is worse than a tenant that takes another ten seconds.

Athena is the part I enjoyed most. She is an assistant sitting inside the ERP who can be asked things like "why is gross profit down" in plain English, and the interesting thing is how little magic is involved. The backend runs an in-process registry of tools, resources and prompts, which is the shape of an MCP server without the transport. The intent classifier is the router. Each module registers read-only lookup tools that run real SQL against real tables, and every one of them passes through a permission gate that fails closed at every step: no caller, no enforcer, no grant, no answer. The gate lives in exactly one file, because it had already been written twice and the two copies had started to differ, and a permission check that differs between two tools is not a style problem. It is the precise shape of the bug where Athena answers a question that the screen would have refused.

She also has a small investigation budget. Originally one question bought exactly one tool call, which meant she could explain a module or show its figures but never both, so asking why gross profit fell got you a report restated back at you. Now she gets up to four steps, and at each step she sees everything gathered so far and decides whether to call one more allowed tool or answer now. The allowlist is deny by default, so a tool registered after that file was written is unreachable from an investigation until someone deliberately adds it. Every module also ships a JSON manual embedded into the binary and exposed as a resource, which is how she explains what a goods received note is without inventing a definition. Under every answer there is a "say it simpler" button, and every tap is logged, which quietly produces a ranked list of the answers real people could not follow. That list is worth more than any readability score.

The document parsing is the other AI piece, and it earns its place. A mailbox poller watches an IMAP folder for incoming purchase orders and invoices, Textract lifts the text off the PDF, and a model parses the whole document into a structured shape: both parties, every bank account quoted, the payment terms, every tax line, every total, every line item. The goal was never the six fields you need to post a ledger entry. It was to reconstruct the document well enough that a human reviewer can check the parse against the original at a glance. Every field is best effort and anything missing stays at the Go zero value, with one rule written in capitals in the source: downstream code must treat a zero as absent and never as a real zero. The mailbox poller is also off unless explicitly enabled, because a misconfigured mail poller that authenticates as the wrong account is not a bug, it is a data leak with a cron schedule.

Foreign currency is where an ERP in Zimbabwe either works or quietly destroys your books. There is one rate store, one endpoint that serves it, and rates are held as foreign units per one base unit. The reciprocal is taken in exactly one function, server side. No frontend inverts a rate and no form asks a user to type one, because a form that asked for "FX rate to base" sitting next to a screen that asked for "units per 1 USD" is how a 702 times error walks into the ledger. No frontend defines a currency list either, and a currency with no live rate simply is not selectable, which is the entire point: it cannot then be booked at par. The codebase had grown four separate currency lists once. It will not grow a fifth.

ZIMRA fiscalisation is built in rather than bolted on. The platform registers as a virtual fiscal device against the revenue authority's FDMS, so receipts and invoices are submitted as they happen, fiscal days open and close properly, and the VAT return is prepared from live data instead of a spreadsheet at month end. Receipts print over ESC/POS thermal printers carrying the fiscal QR code and the VAT breakdown, which means a sale is compliant at the counter at the moment it is rung up.

Underneath the accounting there is a full ledger producing IFRS aligned statements on demand: trading account, income statement, balance sheet, cash flow, changes in equity, trial balance, general ledger. Then the optional instruments work, which is where the finance degree finally paid rent: debt instruments under IFRS 9, leases under IFRS 16, equity and a cap table under IAS 32, dividends. Fixed assets get a proper register with depreciation runs, revaluations, transfers, disposals, physical verification and capital allowances. Payroll runs Zimbabwe's statutory returns, the P2, the ITF16 and the NSSA P4, straight from the run data. Non-profits get fund accounting, budget versus actual, donor reports and consolidation, because half the organisations that needed this are NGOs and telling them to use a for-profit chart of accounts is how you get a spreadsheet parallel to the system.

Then the modules I did not expect to write. Mining, with a gold room, metal balance, stockpiles, shift logs and safety. Construction, with houses, estimates, progress claims, worker agreements and a rule book. Fleet, with dispatch, consignments, fuel, tyres and cost per kilometre. Manufacturing with work centres and standard costs, warehouse with a floor and a layout, planning with buffers, treasury with a cash position and a forecast. Each one started as a customer saying "this is nearly right, except our business is actually about rocks", and the general ledger underneath them never had to change, which is the strongest evidence I have that the core was modelled correctly.

Cyclopes is my favourite small service. It renders HTML to PDF with WeasyPrint, one job at a time off RabbitMQ, uploads to a private S3 bucket and calls the producer back. It is shared across every tenant and keeps nothing locally. The rule that makes it trustworthy is that it never requeues to the main queue, so there are no poison loops: a message ends as an ack, or parked for replay on a separate queue when the render worked but the callback could not be delivered, or dead lettered when it was garbage. Replay is safe because the S3 key is deterministic, so rendering the same job twice writes the same object. Callback authentication needs no shared config at all: the producer mints a random token, keeps it against the job, ships it in the message, and Cyclopes echoes it back in a header for the ERP to compare.

The POS app is Expo and React Native, built for a counter rather than a desk. Search or scan, tap to add, and pay in cash with change calculated, or card, or EcoCash, OneMoney, InnBucks and Telecash, or a voucher, or several of those split across one sale, or in a different currency at the live rate. Voiding a sale needs a manager, which is less about security theatre and more about the fact that tills are operated by people having a bad Tuesday.

The one thing I would tell anyone starting something this size: the boring decisions are the ones that saved it. One rate store. One permission gate. One deterministic S3 key. Every single time I let a second copy of something exist, it cost me a week later.

Key Features

  • Eleven switchable modules per workspace, from point of sale to fund accounting for NGOs
  • Automatic tenant provisioning: a dedicated application and Postgres stack per customer on Docker Swarm
  • Athena, an in-process MCP style registry of read-only tools, resources and prompts the ERP can be questioned through
  • A bounded investigation loop giving one question up to four tool calls before it must answer
  • Per module JSON manuals embedded in the binary, so explanations come from documentation rather than invention
  • Permission gates on every AI lookup that fail closed at each step, written once and shared
  • A say it simpler button under every answer, which doubles as a ranked log of answers people could not follow
  • Invoice and purchase order parsing: an IMAP poller, Textract, then a full structured reconstruction of the document
  • ZIMRA FDMS fiscalisation as a virtual fiscal device, with fiscal days, VAT returns and QR coded receipts
  • IFRS aligned statements on demand, plus IFRS 9 debt, IFRS 16 leases and IAS 32 equity instruments
  • Fixed asset register with depreciation runs, revaluations, disposals, verification and capital allowances
  • Zimbabwean payroll producing the P2, ITF16 and NSSA P4 returns from live run data
  • Mining, construction, fleet, manufacturing, warehouse, planning and treasury verticals on one ledger
  • Multi-currency throughout, with one rate store, one reciprocal function and no client side rate maths
  • An Expo point of sale app with barcode scanning, split payments, offline tolerance and ESC/POS fiscal receipts
  • Cyclopes, a shared stateless PDF renderer with deterministic S3 keys, parked callbacks and no poison loops
  • Hermes, a WhatsApp gateway resolving a phone number to a tenant for payslips, statements and balances

Challenges & Solutions

Provisioning a whole isolated stack per tenant without the provisioner ever touching a tenant database

Refusing to reload the nginx ingress until the new tenant's own readyz endpoint answers

Four separate currency lists had grown in one codebase, and a form asking for an inverted rate put a 702 times error into a ledger

Giving an AI read access to production accounting data without it ever answering a question the screen would refuse

One question buying exactly one tool call, so Athena could show the figures or explain them but never both

Parsing an invoice completely enough that a reviewer can check the parse against the original document

A PDF worker that must never requeue to its main queue, so every message acks, parks or dead letters

Keeping one general ledger correct while mining, construction and fleet each insisted their business was different

Key Learnings

A permission check that differs between two code paths is not duplication, it is the bug where the AI overshares
Deny by default on an AI tool allowlist means tools added later are unreachable until someone decides otherwise
A deterministic S3 key turns retry safety into a naming decision instead of a distributed lock
Storing rates in one direction and inverting them in exactly one server side function removes an entire class of error
Zero values from a document parse must be treated as absent, because a real zero and a missing field are not the same claim
Every time I let a second copy of something exist, it cost a week later

Project Info

Timeline

Ongoing, over a year in

Team

Solo build for Dryback Technology

Status

Live

Tech Stack

Backend

GolangFiberGORM

Frontend

ReactTypeScriptViteNext.jsTailwind

Mobile

React NativeExpo

Data

PostgreSQLRedisRabbitMQ

AI

OpenAIMCPTextract

Infrastructure

Docker SwarmNginxAWSS3

Documents

WeasyPrintPython

Compliance

ZIMRA FDMSIFRS