skip to content

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
A builder standing beside one glowing database core, with three sealed glass lanes branching out of it, each holding a different company's records and none of them touching.

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

The same builder amid floating system panels — a booking calendar, a roles table, an invoice, a webhook arriving twice and a queue of job tokens — each wired down to one database core.

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.

  1. constrain

    what must never happen — a double booking, a crossed tenant, a card charged twice

  2. model

    the schema that makes those things impossible, written before a single endpoint exists

  3. build

    NestJS and Node on PostgreSQL, permissions checked at one edge, jobs safe to run twice

  4. run

    deployed to AWS with the pipeline that puts it there, so shipping is a Tuesday rather than an event

Two identical booking requests arrive at a database core from opposite sides. The builder sets a constraint plate into the core, and the second request bounces off it.

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.

On the left, tangled pipes crossing between three customer boxes with records spilling out. On the right, the same three boxes on clean separate lanes, with the builder closing the last one.

“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
The builder pushing a sealed system slab up a short conveyor into a cloud platform, with a small pipeline of three stages running behind it.

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.

Book a callEmail