All posts
SaaSArchitectureMulti-tenancyNext.js

Multi-tenant SaaS: what I actually had to figure out

Published October 30, 2025 · 6 min read

BookUp started because I was annoyed

Every barbershop and salon around here runs bookings out of Telegram DMs and a paper notebook. It works until it really doesn't — double bookings, missed appointments, no way to see your week at a glance. So I built the thing I wished they had.

The business idea is simple: one platform, many businesses. Each business gets its own booking page, its own schedule, its own client list. Customers book without creating an account. Businesses manage everything from a dashboard.

The tricky engineering part is making "each business gets their own space" feel real. That's multi-tenancy, and here's what it actually involves.

Subdomains: the part the customer sees

When a customer books at Mustafa's barbershop, they go to mustafa.bookup.uz. Not bookup.uz/businesses/mustafa — that URL says "you're renting space in someone else's app." A subdomain says "this is Mustafa's place."

In Next.js this runs in middleware. Every request comes in, I read the host header, strip the base domain, and get the tenant slug. Then I rewrite the URL to carry that slug as a route segment internally.

The nice thing: wildcard DNS handles this for free. One DNS record, every subdomain routes to the same server. When a new business signs up and picks a slug, they're live instantly — no infrastructure work, no waiting.

Data isolation: the part that bites you

One database, shared tables, every tenant row has a tenant_id column. This is the standard approach and it's the right call at this scale — one migration to run instead of N, simpler backups, simpler monitoring.

The part that bites you if you're not careful: you forget to add the tenant filter somewhere. You build a "recent bookings" query, it works great in dev with one tenant's data, and then in production you're accidentally showing one business's bookings to another business's dashboard.

My fix: I put the tenant context into the NestJS request scope and inject it into every service. The service layer can't make a query without knowing which tenant it's working for. It's not perfect but it catches the obvious mistakes.

What I wish I'd done from day one: enforce this at the module level, not the service level. I spent a couple of weeks refactoring code that had grown tenant-agnostic query helpers before I caught the pattern.

Onboarding: don't leave people in an empty room

New tenant signs up, picks their slug, lands on their dashboard. Empty schedule. No services listed. No staff. The worst thing you can do is show them a blank screen with a "Get started" button.

What I did instead: create three sample services and a placeholder staff member automatically. The tenant sees what their booking page looks like with data in it. They can delete the samples, but the first impression is "oh, this is what it does" instead of "where do I even begin."

Small thing, made a big difference in how many tenants actually set up their account after registering.

What I'd tell my past self

The tenant ID needs to live in your auth token, your request context, and your query builder from day one. Retrofitting it is a weekend of annoying work.

Subdomains beat path prefixes. The URL communicates ownership, and ownership makes the product feel premium for zero extra engineering.

The onboarding empty state is the first real user experience. Invest there early.