02 / system · The system under it
Multi-tenant SaaS developer: scheduling, bookings, payments and roles
The half of your product nobody sees until it breaks. NestJS and PostgreSQL, where the database enforces the rules instead of trusting your code to remember them.
- Three multi-tenant products in production
- Driving schools, yacht charter, study abroad
- Sole engineer on Gradsy: schema, API, deploy

what I build
The half of the product that has to be right.
One deployment, many paying customers, and rules the database refuses to break. None of it is visible until the day it is.
SaaS platforms
one product, many paying customers, kept apart and billed separately
multi-tenant apps
one deployment, many organisations, and no data crossing between them
booking systems
two people cannot take the same slot, and the database is what stops them
APIs
endpoints your own frontend and someone else's client both call
auth and roles
who can do what, checked in one place at the edge of the system
payments and billing
subscriptions, invoices, and webhooks that arrive twice
background jobs
the work that runs at 3am, written so a second run does nothing
third-party integrations
someone else's API, retried through the hours it is down

how it goes
Constraint, schema, API, run.
I ask what the constraint is before I ask what to build. The answer changes the schema, and the schema is the expensive thing to change later.
constrain
what must never happen — a double booking, a crossed tenant, a card charged twice
model
the schema that makes those things impossible, written before a single endpoint exists
build
NestJS and Node on PostgreSQL, permissions checked at one edge, jobs safe to run twice
run
deployed to AWS with the pipeline that puts it there, so shipping is a Tuesday rather than an event

why postgres
A rule belongs in the database if the database can hold it.
Application code checks a rule. A database enforces one. The difference shows up the day two people click the same button in the same second.
- one unique constraint
- two people book the same slot. The second write fails, and that person is told it has gone.
- states in a table
- the moves an application may make are rows you can read, not an if chain spread across three services.
- jobs that repeat safely
- a queue delivers at least once, which sometimes means twice. Charge once, email once, count once.
- tenancy in the schema
- the subdomain scopes the query, so an if statement is never the thing keeping two customers apart.
- one place for permissions
- who is allowed to do this is a question you answer by opening one file.
- migrations that reverse
- a bad release rolls back the same way it went out, instead of becoming an incident.
the problems I solve
You have probably said one of these out loud.

“We sold to a second company, and now there is an if statement deciding whose data comes back.”
It works, and it works until someone forgets the if. I move the separation into the schema, where forgetting is not an option the code has.
“Two customers booked the same slot. We refunded one and nobody knows why it happened.”
Your API read the slot, then wrote to it. The second booking got in between those two steps. A unique constraint closes that gap; no amount of checking in code does.
“The payment provider sent the same webhook twice and we charged twice.”
At-least-once is how every queue and every webhook works. Every job I write does nothing on the second run.
“Nobody on the team wants to own the boring half.”
The permissions, the billing edge cases, the job that runs at 3am and has to be right. That is the half I take.
“One person understands this system and none of it is written down.”
I document while I build — the schema, the roles, the jobs and the reasons behind the awkward decisions. I have been that one person, and it is a bad place to leave a team.
- NestJS
- Node.js
- PostgreSQL
- Prisma
- Redis + BullMQ
- AWS
Schema first, then the API, then the interface. I build the screens on top as well, so there is no seam where my work stops and somebody else's is supposed to start.
how I build it
A rule belongs in the database if the database can hold it.
Two counselors open the same appointment slot and both click book. Postgres lets both of them read that slot before either one saves. Both see it free. Both take it. No amount of checking in your API code closes that gap, because the gap sits between the read and the write, and it is as wide as your traffic is heavy. A unique constraint on the table does close it. The second write fails, and the second person is told the slot has gone.
State works the same way. An application that moves through stages gets a table of states and the moves allowed between them, not a text column and a pile of if statements spread across three services. Then "can this go from submitted to accepted" is a row you can look at instead of a code review you have to run.
Queues promise to deliver a message at least once. In practice that sometimes means twice. So every background job is written to do nothing on the second run. Charge once. Email once. Count once.
Schema first, then the API, then the interface. Migrations run backwards as well as forwards. Permissions are checked in one place at the edge of the system, so "who is allowed to do this" is a question you answer by opening one file.
when a project needs more than me
I deploy and run the infrastructure most products need. Some projects need more than that: dedicated platform work, heavy data pipelines, an environment with real operational demands. For those I bring in senior infrastructure engineers I already work with.
The database and the application stay with me. The platform goes to somebody who does it full time. You are still dealing with one arrangement rather than three, and one person answerable for how it turns out.
what working together looks like
You bring it. I handle it. You get it back better.
you bring
- a product with a second customer, or a system that broke once
- the rule that must never be violated
- whoever answers the support ticket when it is
I handle
- schema design, and migrations that run both ways
- multi-tenant isolation and role-based access
- APIs your frontend and someone else's client both call
- queues, background jobs and retry safety
- payments, invoices and duplicate webhooks
- deployment and the pipeline that does it
you get
- your code, in your repository, from the first commit
- documentation written while I build, not promised for the end
- a system where the rules are enforced by the database rather than remembered by people

Something in your system that must never break?
Thirty minutes on what you are running and what went wrong with it. I will tell you where the rule belongs and whether I am the right person to move it there.
