A bespoke ERP built around your approvals, your documents and your branches
Multi-module ERP systems on React and Postgres: roles and branch scope enforced in the database, an audit log nobody can rewrite, and your own repository from the first commit.
ERP covers a lot of ground, so to be specific: this page is about the system that holds the books, the stock, the people and the approvals of one business, replacing some combination of a desktop ledger, a few spreadsheets and a notebook. We have built two. One is eighteen modules for a regional trading and distribution group, replacing a legacy desktop ledger. The other covers students, fees, payments, payroll and accounting across the branches of an independent school group.
What they share is not a feature list. It is that the valuable part was the rules that must hold: who may see which branch, what counts as revenue only once it is approved, and who changed which figure and what it was before. Those rules live in the database, not just in the screens, because screens get refactored and a rule that only one screen enforces is a rule that eventually is not enforced.
We should also say where this page is going. The default answer to ERP is buy, and for most businesses that is right. A good part of what follows is how to tell whether you are one of the few for whom the arithmetic has flipped.
What the two builds replaced
Both builds started from specific things going wrong rather than from a wish for software. These are the four that came up.
A ledger on one machine
The trading group's books lived in a proprietary desktop accounting application: one machine, one copy, no remote access, and an export path that produced nothing usable. Three years of purchase and sales history were effectively trapped inside it.
The same invoice keyed twice
Supplier invoices arrived on paper and were re-keyed by hand into both the ledger and a stock spreadsheet. The two disagreed constantly, which is what happens when one fact lives in two places and a person is the only link between them.
No answer to who changed it
When a figure was wrong there was no way to establish who had changed it, when, or what it had been before. Nothing in the old system was built to remember.
Branches that each need their own view
The school group needed every branch to see its own students, fees and staff, head office to see all of them, and no leakage between sites. Fee collection depended on someone remembering to chase, and the record of who had been chased lived in a notebook.
What a bespoke ERP is made of
Six pieces that appeared in one or both builds. Not every business needs all six, and the list is the order we would argue about them in.
- 01
Permissions enforced more than once
On the trading build, roles are a grid of eighteen modules against five actions, editable per role and overridable per person. Navigation hides what you cannot view, routes redirect, the write layer checks before touching the database, and a trigger discards any change a non-administrator makes to their own role or permissions. The layers above the database are the application being tidy; the trigger is what stops privilege escalation. On the school build the same idea carries a second dimension: row-level security policies for both role and branch, so a branch administrator's queries return their branch's rows regardless of which screen asked.
- 02
Approvals that gate the numbers
Invoices move draft, pending review, approved, unpaid, partial, paid, and the approval step is not decorative: drafts and invoices under review are deliberately excluded from revenue, purchases and outstanding totals, on the lists and on the dashboard, because nothing is owed until an invoice is approved.
- 03
An audit trail that survives the application
Every write goes through one data-access layer that stamps the acting user and appends a field-level diff to an append-only table. Staff can select and insert there and nothing else, so the trail cannot be rewritten through the API, even by the application itself. Deleting sets a timestamp rather than destroying the row; only an administrator can purge, and the audit entry outlives the record.
- 04
One ledger, so the reports reconcile
On the school build, students, fee invoices, payments, expenses, employees and payslips feed a single accounting layer. A fee receipt and a salary payment land in the same books, and the reports reconcile without a spreadsheet in between.
- 05
Capture and reminders where the work happens
Supplier invoices on the trading build are photographed and read by a vision model, with a person confirming before anything is written. Fee reminders on the school build go out over WhatsApp from templates, so collection is a scheduled action instead of a remembered one.
- 06
Installable, and still open when the signal drops
The trading ERP installs as a progressive web app, runs full-screen from the home screen and still opens cached pages when the warehouse loses signal. It was a requirement because staff use it on phones in a warehouse, not because it is fashionable.
The part that moves the estimate most
An ERP that starts empty is the easy version. Most of the businesses that ask for one have years of records in something else, and the question of whether those can be got out decides how much of the budget goes on the migration instead of on the ERP.
On the trading build the desktop files turned out to be SQLite databases whose records were protobuf blobs. We wrote a decoder and recovered inventory, customers, suppliers and the full purchase and sales invoice history with line items, quantities and rates, then merged entities across books that used different identifiers. The date epoch was calibrated against a known paper invoice, and one document was checked to decode to the correct date with all seven line items matching the printed amounts exactly. Loads were made idempotent, so a re-run corrects rather than duplicates.
That is one system's quirk, not a general promise. Another desktop product will fail differently, and some will export cleanly. What generalises is the method: reconcile the recovered data against something you can independently verify, and do it before the new system is trusted with the live books.
How long it takes and what moves the number
Most of our projects land between $5,000 and $10,000 (US dollars). Larger systems cost more than $10,000. That is a typical range, not a quote: we quote after a discovery conversation, once we have seen the process, its exceptions and the systems involved. Neither ERP case study on this site states a price or a duration, so we do not attribute the range to either of them. What we can set out is the structure of an estimate: three things move it, and they are knowable before anyone writes a quote.
- How much of the business is exception rather than happy path. An approval chain with three variants costs more than one with a single path, and each document type is a case to build and test.
- How many systems still have to agree, meaning how many believe they own the same data. An ERP that replaces the ledger is cheaper than one that sits beside an accounting product you are keeping, because the second is a reconciliation as well as an application.
- Whether the rules need a database underneath. For an ERP they always do, which is why it is a build rather than a configuration.
- Historic data, which sits on top of the three. The old records have to be got out, mapped and proved, and on the trading build that was a piece of work in its own right.
When a custom ERP is the wrong answer
Buy, wherever a product genuinely fits. A product has already absorbed the cost of edge cases you have not thought of, has a support obligation, and improves without you paying for each improvement. The rule we use is three costs summed over the horizon you plan to keep the system: licence drift (the seats you expect in each year, times the seat price, plus any tier upgrades), the integration tax (the hours a month spent making several tools behave as one, times what those hours cost), and workaround cost (the recurring price of every step that exists only to compensate for the tool's shape). Set that total against build cost plus running costs over the same horizon. Build when the first is larger, or when the process is what makes you different. Otherwise buy.
Building is also wrong where the process is undifferentiated. If doing it slightly differently from everyone else has no commercial value, you would be spending money to acquire maintenance. And it is wrong where nobody will own the result: a build you cannot hand to another competent engineer without an introduction was built badly, which is why the code lives in your repository and on your accounts from the first commit.
The option in the middle is usually the right one. Keep the accounting product and build the capture pipeline in front of it, or keep the database and build the interface your team needs. If you are small, that is also the honest answer to cost: a first module is a far smaller commitment than a replacement for everything.
Related projects that were not this job
Here for the argument rather than as evidence of this exact work. Each one bears on a decision above, and the note says which.
The disciplines behind it
Platforms that bear on it
Related reading
Custom ERP development, specifically
Most of our projects land between $5,000 and $10,000 (US dollars). Larger systems cost more than $10,000. That is a typical range, not a quote: we quote after a discovery conversation, and neither ERP case study states a price, so we do not attribute the range to either. Three things move the number: how much of the business is exception rather than happy path, how many systems believe they own the same data, and whether the rules need a database underneath, as an ERP's always do. Historic data that must be migrated and reconciled sits on top. For scale, our build-versus-buy article says a full application, such as an ERP module set, typically runs eight to twelve weeks. Send the exception list, the systems involved and whether your history can be exported today, and the reply can say which variable would move it.
Buy, unless the arithmetic says otherwise. Add up licence drift, the integration tax and workaround cost over the horizon you plan to keep the system, and set that against build cost plus running costs over the same horizon. The horizon is your choice. Building is justified when the first total is larger, or when the process is what makes you different from your competitors. The build-versus-buy article sets the method out in full.
Sometimes, but often the honest answer is to start smaller. A full replacement is a substantial commitment, and a small team may be better served by buying the commodity parts and building one module, such as the capture pipeline for supplier invoices or the portal a product cannot provide. We would rather tell you that on the first call than sell you the larger build.
Usually, and the first step is looking at what the system will give up before quoting. On the trading build the old data sat in a proprietary desktop format, which we decoded and then verified against a paper invoice. Whatever the format, the migration is reconciled against something independent, and loads are idempotent so a re-run corrects rather than duplicates.
You own it, in your own repository from the first commit, and it is deployed on your accounts: your Vercel project, your Supabase project, your domain. If you take the work in-house or move to another partner, nothing needs to be untangled first.
Page copy drafted with AI assistance. Skofi is accountable for it. Editorial policy
Tell us about the process that keeps costing you.
A 15-minute call is usually enough to say whether this is an automation, an integration, or something that should be built, and whether we are the right people for it.
What happens on the call
- 15 minutes, no deck
- We map the process on the call
- A written scope, or an honest no