Why we built our own database (and why you probably shouldn't)
AxisIQ runs on a SQL engine we wrote in C, giving each customer organisation its own database. What that bought, what it cost, and when not to copy it.
Most companies should treat the database as a solved problem and get on with their product. AxisIQ did the opposite: the platform runs on a SQL storage engine we wrote ourselves in C, and every customer organisation gets its own database rather than a slice of a shared one. This essay is about what that bought, what it cost, and why it is the wrong default for nearly everyone reading it.
The advice in the title is not modesty. If you are weighing up whether to write a storage engine, the answer is almost certainly no — and the reasons are worth more of your time than our reasons for saying yes.
Start with the part you should not copy
A mature database hands you decades of adversarial testing for free. Every way it fails has already been hit by someone else, written up, and answered. The tooling exists. The people who know it exist. The extension you need on a Thursday afternoon exists.
Give that up and you buy all of it back with your own time. Roughly a year of engineering went into ours before it did anything a customer could see on a screen, and none of that year appears on a feature list. The operational knowledge — what the logs mean, which failure looks alarming and is not, how to recover the ugly case at two in the morning — we have had to learn the hard way, on our own hardware, at our own expense.
So the honest test is simple. If your product would be the same product on PostgreSQL, use PostgreSQL. That covers almost every company.
What we actually run
AxisIQ runs on a SQL storage layer written in C, speaking a compact binary protocol to the rest of the platform. The parts worth naming:
- Real tables per object type. When you define a custom type — consignments, repairs, site visits — you get a genuine table with genuine columns, not rows in a key-value bag pretending to be a schema.
- UUID v7 primary keys throughout, generated by the application, so identifiers are unique across databases and roughly time-ordered.
- A write-ahead log, so a committed write survives the machine losing power mid-sentence.
- B-tree indexes, online backup, and point-in-time recovery to a chosen moment.
- Encryption at rest and in transit — AES-256-GCM on disk, TLS between the application and the engine.
- Native vector search and generated columns, which power meaning-based search over your documents and computed fields.
- A query planner young enough that one engineer can still read all of it.
That last one is not a feature. It is the reason for everything below.
The forcing function was isolation
There are two ways to keep one customer's data away from another's. You can make it a column — an organisation identifier on every row, and a filter on every query, forever. Or you can make it a boundary.
We took the boundary. This is the pattern usually called a database per tenant: each organisation's data lives in a database of its own, so a query cannot reference another organisation's tables — they are not there to be named. There is no clause to forget.
The usual objection to a database per tenant is operational: many databases to provision, back up and watch instead of one. That cost is real and we pay it. We would rather pay it there than in a filter clause on every query the product will ever write.
This mattered most somewhere we did not anticipate. Analytics lets people write real SQL against their own live operational data. On a shared schema, that feature is a standing bet that every query rewrite is correct, every time, for as long as the product exists. On a database that contains one organisation and nothing else, the remaining job is much narrower: hide the fields a particular person is not allowed to read. Each table is swapped for a masked version where denied fields come back as NULL, and the query is checked before it runs — read-only, a single statement, no reaching outside the workspace.
The risk does not go to zero. But a bug in that layer means someone sees their own data slightly wrong, rather than seeing a stranger's.
Afternoon changes instead of feature requests
Owning the engine changes what a product decision costs.
When the lists and reports people read all day needed the newest handful of rows without sorting an entire table first, that was a change to our planner — a fast path for a query the product runs constantly. On someone else's engine it is a rewrite, an index guess, or a wait.
When Work Queues needed claiming to be genuinely atomic — two packing stations pulling from the same queue must never be handed the same carton — we did not have to design a lock protocol above the database. Selecting an item and marking it claimed happens in one transaction, and the engine's own locking guarantees two transactions can never both claim it.
Neither of those is heroic. That is the point. The shape of owning your stack is that the thing your product needs is a change you make on a Tuesday, rather than a request on a roadmap you do not control.
The numbers, honestly
Reads are fast in the way in-memory reads are fast: millions of rows a second once the data is resident. That is the workload the product actually has.
Durable writes are the opposite, and we would rather say so plainly. A committed write waits for the disk to confirm it, which lands around a thousand a second on ordinary hardware. That is a property of durability, not of cleverness — anything faster is a system that has quietly agreed to lose your last few writes.
We accept the trade because business operations are read-heavy. An order is written a handful of times and read hundreds. If your workload is constant high-rate ingestion — telemetry, clickstreams, sensor data — this design is the wrong shape and we would tell you that before you signed anything.
What is missing
Two gaps, stated as gaps:
- No automatic failover yet. Backup and point-in-time recovery are real and tested, and follower replicas serve reads. A standby that takes over by itself is not built.
- Indexes are RAM-resident. Memory is a real sizing constraint today, not a theoretical one.
The roadmap for those is real and unfinished, and we are not going to invent a date for it here.
The argument that actually held
The case was never that our engine beats a mature one. For most workloads it does not, and it does not need to.
The case was that a few of the promises we wanted to make are properties of the storage layer and cannot be convincingly bolted on above it. Your data is physically separate from every other customer's. A queue item cannot be handed to two people. Reporting runs on live records with permissions applied, not on last night's export. Build on someone else's engine and each of those becomes a feature request on someone else's roadmap, with your product waiting behind it.
That is the only version of this decision we would defend. Not speed, not novelty — the ability to keep a promise ourselves.
When it is right for you
Four conditions. All of them, not three:
- The guarantee you need lives at the storage layer and genuinely cannot be built above it.
- You are otherwise fighting your database's model rather than using it.
- You can afford a long stretch where the work does not show up in the product.
- Your workload is narrow enough that you do not need a general-purpose engine's entire surface area.
We met all four, and it still took a year. If you meet three, use the boring database and spend the year on your customers — which, in a different form, is the same argument we made for building AxisIQ at all.
FAQ
What database does AxisIQ run on? A SQL storage engine we built in C, with one database per customer organisation. Every record, report and ledger entry in your workspace sits in it, with write-ahead logging and point-in-time recovery behind them.
Should I build my own database engine for my SaaS? Almost certainly not. Unless the guarantee you need is genuinely at the storage layer, a mature engine gives you decades of hardening, tooling and hiring pool that you would otherwise pay for in your own engineering years.
How is my data separated from other AxisIQ customers? By boundary rather than by filter — your organisation has its own database, so a query has no way to name another organisation's tables. Inside it, permissions decide which records and fields each person can read.
Can I write my own SQL against my data?
Yes. Queries run on your live data through Analytics, restricted to a single read-only statement, with fields you are not permitted to see returned as NULL.
Is a custom engine risky for a business system of record? It is a risk we take seriously rather than dismiss. Durability comes from write-ahead logging, recovery from online backups with point-in-time restore, and the honest gaps — automatic failover, indexes beyond memory — are listed above rather than buried.