---
title: "Multi-tenant SaaS developer: scheduling, bookings, payments and roles"
description: "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."
url: https://riteshkc.com.np/services/multi-tenant-systems
source: https://riteshkc.com.np/services/multi-tenant-systems.md
updated: 2026-08-27
site: "Ritesh KC"
---
# 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.

- Layer: 02 / system
- Builds: 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)
- Tools: NestJS, Node.js, PostgreSQL, Prisma, Redis + BullMQ, AWS
- Three multi-tenant products in production
- Driving schools, yacht charter, study abroad
- Sole engineer on Gradsy: schema, API, deploy

## 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.

## 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.

## Constraint, schema, API, run.

01. constrain — what must never happen — a double booking, a crossed tenant, a card charged twice
02. model — the schema that makes those things impossible, written before a single endpoint exists
03. build — NestJS and Node on PostgreSQL, permissions checked at one edge, jobs safe to run twice
04. run — deployed to AWS with the pipeline that puts it there, so shipping is a Tuesday rather than an event

## 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

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.

## Proof

- [Gradsy](https://riteshkc.com.np/work/gradsy): A centralized platform for students, counselors, and admins with role-based access - built with Next.js and NestJS, and deployed on AWS using S3, SES, and Lightsail. Stack: Next.js, NestJS, TypeScript, Prisma, PostgreSQL, Redis, BullMQ, AWS, AWS Simple Email Service, S3, Lightsail.

## Related writing

- [Overbooking is a database problem](https://riteshkc.com.np/blog/overbooking-is-a-database-problem): Counting rows before you insert one is not a capacity check. Here is how a unique index, a constraint violation, and a NULL ended up doing the work instead.
- [A status machine belongs in a table](https://riteshkc.com.np/blog/a-status-machine-belongs-in-a-table): Nine statuses, two sets of allowed transitions, and two names for every state. Moving all of that into rows instead of an if chain got engineering out of the workflow business.
- [Rate limiting a provider without a rate limiter](https://riteshkc.com.np/blog/rate-limiting-by-queue-shape): SES caps sends per second. The fix was worker concurrency of one and a sleep, plus an honest note in the code about the case where it stops working.
