Engineering

The architecture behind MechIQ

A multi-tenant platform covering everything a garage does — booking, the workshop, parts, invoicing and payments — with AI through the middle of it, on infrastructure built to look after itself.

Kevin Moore

Kevin Moore

Co-Founder & CTO, MechIQ · LinkedIn

3 October 2026 · 10 min read

This is the technical companion to how MechIQ was built, which covers why it exists and who built it.

Four things shaped nearly every decision here, so they're worth stating before the detail:

  • It covers everything a garage needs: a vehicle's whole journey through the workshop, and the parts, invoicing, payments and customer records needed to run the business around it.
  • It holds that money and those records for businesses that compete with each other, in one system.
  • It's maintained by one engineer, and its customers are independent garages, which puts a ceiling on what the infrastructure can cost to run per garage.
  • AI sits in the middle of the product instead of beside it.

Most of what follows is downstream of one of those four.

Tenancy: isolation the application can't forget

Every garage shares one PostgreSQL database. A database per customer buys isolation, but every schema change then has to be applied and verified across hundreds of separate databases, and the bill grows with the number of customers, not with how much they use it. Sharing one database also puts the system's most dangerous failure right at the centre: one query missing one filter and a garage sees a competitor's data.

With one engineer, "remember to scope every query" is not a control. So the application isn't trusted to. Every table holding garage data carries a tenant_id and a row-level security policy, each request sets its tenant for the transaction, and Postgres filters from there. The role the application connects as cannot bypass those policies. A forgotten filter returns zero rows instead of somebody else's.

Foreign keys are composite, on (tenant_id, id) rather than id. Without that a job line can point at a part belonging to another garage and the database will accept it, because the key is valid. With it, the relationship itself has to stay inside one tenant.

A static check fails the build if a new model stores a garage's data without being declared tenant-scoped, or a migration creates such a table without a policy. The convention and the thing that enforces the convention arrived together, because the second is the only part that works at midnight.

How a request is isolated: the token carries the garage, the application sets the tenant for the transaction, and a Postgres row-level security policy filters every tenant-scoped table. A query that forgets its filter returns zero rows.
The failure mode is the point: a forgotten filter returns nothing, never another garage’s rows.

The stack, and the cost constraint

Python and FastAPI on the backend with SQLAlchemy 2.0 in async mode. React, Vite and TypeScript for the web app, plus a separate admin application for platform operations. The technician native app is React Native through Expo, published to both the Apple and Google stores, so one codebase covers iOS and Android and fixes ship over the air without an app update.

On AWS: ECS Fargate for the two services, RDS PostgreSQL 16 across two availability zones with automatic failover, ElastiCache Redis for caching and settings, CloudFront in front of the static apps, Cognito for authentication, S3 for files. All defined in CDK, so the environment rebuilds from the repository rather than from memory.

It runs for a few hundred euro a month. That number is a design input, not a result. Selling to independent garages caps what the infrastructure can cost per tenant, which rules out a lot of architectures that would otherwise be reasonable, and rules in boring, well-understood managed services over anything clever.

Production infrastructure on AWS: CloudFront in front of the marketing site and both web apps, an application load balancer spanning two availability zones, ECS Fargate running the main and admin APIs in each, RDS PostgreSQL 16 with multi-AZ failover, ElastiCache Redis with a replica, and S3, Cognito and Secrets Manager as regional services.
Two availability zones, automatic failover on the database and cache, and the whole thing defined in CDK.

AI in the middle

Starting from nothing is what let us build on current technology instead of working around foundations laid for a different era.

AI isn't a feature in a menu here, it is central to MechIQ. It drafts a quote from a customer's description and the vehicle's details, builds the work plan a technician follows, reads and enters a supplier invoice photographed on a phone, writes the first version of the message that goes to a customer. The data model assumes it: a quote line records whether its price came from a catalogue, a previous purchase or an estimate, because the interface has to show which.

Every call goes through one service, not scattered SDK calls. Retries, response parsing, structured output and cost logging all live there, so a change to prompt handling or a model deprecation is one change, not twenty.

Quotes are grounded in industry-leading manufacturer data rather than invented. The model works from that data, and the interface says plainly when a figure is an estimate and not a published book time.

If the AI can't be reached or returns something unusable, the system falls back to a heuristic that's worse but functional, and the user is told the answer is degraded. A degraded answer nobody notices is how a garage quotes badly for a fortnight.

Delivery: one build, promoted by digest

A change merged to main builds once. That image deploys to staging, the full browser suite runs against it, and only then is it marked promotable. Promotion to production retags that exact image by its content digest, runs migrations, rolls both services, deploys the front ends and runs a walkthrough against production.

Nothing is rebuilt in between. The artefact that was tested is the artefact that ships, and the container installs from a lockfile so a build can't resolve a dependency differently and put something into production that nobody ever ran.

Migrations run before the roll, and each sets a lock timeout. A schema change that can't take a clean lock on a busy table fails fast and retries instead of holding a lock while a garage is mid-invoice.

Deployment pipeline: merge to main, build the image once tagged by commit SHA, deploy to staging, run the full browser suite, stamp the image promotable, a human merges the promote-to-production pull request, then the same image is retagged by content digest and rolled to production.
One build. The only human step is the promote merge — everything either side of it is automated.

Operations: built to be unattended

One part-time engineer can cover a system around the clock, but only if the system does the watching. Nothing in the design assumes someone is looking, because for most of the week nobody is.

So the system reports on itself. Alarms watch error rates, response times, failed integrations and anything a customer would experience as broken, and they reach a phone, not a dashboard nobody is sitting in front of. The database and cache fail over between availability zones on their own. Work that fails retries instead of waiting to be noticed, and an alarm that fires carries enough context to triage it from a phone before anyone opens a laptop.

Eighteen background loops keep the system moving: reminders, escalating actions nobody has picked up, reconciling invoices, sweeping retries, chasing deferred payments, expiring deposits. Each has a dead-man alarm that fires on absence, not on error. A loop that stops is more dangerous than one that throws, because nothing looks wrong.

The same rule runs through the rest of the system: stop loudly rather than continue against the wrong thing. A missing credential disables its feature and raises an alarm instead of carrying on with something stale.

Testing: the gate, not the paperwork

There are 4,428 tests, and the split matters more than the total.

Breakdown of 4,428 automated tests: 3,036 unit and audit checks running in about ten seconds, 962 integration tests against a real database, 164 web front-end unit tests, 134 full browser journeys across 49 files, and 132 native app tests.
The 134 browser journeys are the gate. The rest support them.

Just over 3,000 are unit and audit checks running in about ten seconds. The audit ones are structural rather than behavioural: every endpoint has a role guard, every model that stores a garage's data is declared tenant-scoped, every migration creating such a table has a policy. They catch the class of mistake that's easiest to make while moving fast.

Another 962 are integration tests against a real database, because the bugs most likely to hurt here are in transactions, row-level security and money arithmetic, and none of those survive being mocked.

Then 134 browser journeys drive the real screens the way a person does: book a job, check a car in, find extra work, get it approved, order parts, invoice, take payment. These are the gate. A core flow isn't covered because a unit test exercises its function. It's covered when the journey passes.

The remainder cover the web and native front ends. All of it runs before any code reaches a garage, which is the only reason one person can change something on a Tuesday evening and still trust invoicing on Wednesday morning.

What this bought

None of this is novel. It's ordinary production engineering applied to a small system, and the only unusual thing is the ratio: a lot of structure for one engineer and a few hundred garages' worth of ambition.

That ratio is deliberate. Structure is what lets a small team move quickly without breaking things it can't see, and the list of things still to fix is never empty. A system this age with a clean one would mean nobody was looking.

The other half of the story

Why MechIQ exists, and how a mechanic and one engineer built it without raising a round.

Read how MechIQ was built