Case Study · Multi-tenancy · 2023 — 2024

Multi-tenancy — one app, isolated tenants by subdomain

Multi-tenant routing that resolves the tenant from the subdomain and isolates each tenant's data.

Spec Sheet

Role

Full-stack engineer

Timeline

2023 — 2024

Status

In production

Surface

Edge middleware + app

Stack

Next.js middleware

Data

Postgres (scoped)

01

Problem


A single codebase needed to serve many tenants — each on their own subdomain — without duplicating deployments, and without any chance of one tenant seeing another's data. The risk in multi-tenancy is not routing; it's the quiet data leak when a query forgets its tenant scope.

02

Context & constraints


A SaaS where each customer gets acme.app.com. Tenant resolution had to happen at the edge, before the app renders, and every data access downstream had to be scoped to the resolved tenant by construction, not by discipline.

Tenant isolationno query can cross a tenant boundary
Edge resolutiontenant resolved before the app runs
Single deployone build serves every tenant
03

Architecture


Request path

Request

subdomain

Middleware

intercept

Parse

tenant slug

Resolve

tenant ctx

Route

rewrite

Tenant data

scoped

Failure recovery & consistency

Unknown sub

no tenant

Reject

404 / marketing

Valid

inject ctx

Scoped query

enforced

04

Implementation


Edge

  • ·Middleware parses host → tenant slug
  • ·Resolves tenant, injects context header
  • ·Rewrites to the tenant-scoped route

Data

  • ·Every query carries a tenant_id filter
  • ·Repository layer enforces scope by default
  • ·No raw model access outside the scope

Routing

  • ·Unknown subdomains → marketing / 404
  • ·Reserved subdomains (www, api) handled
  • ·Single build, dynamic per-request tenant
05

Key decisions


01

Resolve the tenant in edge middleware

Tenant resolution happens before any page or API code runs, so the rest of the app receives an already-scoped context and never has to re-derive which tenant it's serving.

02

Scope by construction, not by discipline

The data layer requires a tenant context to issue a query, so forgetting to filter is a type error, not a silent leak. Isolation is enforced by the API surface itself.

03

One deployment for all tenants

A single build serves every subdomain dynamically. New tenants are data, not deploys — onboarding a tenant never requires shipping code.

06

Tradeoffs


Chose

·Edge-resolved tenant context

·Enforced tenant scoping in the data layer

·Single shared deployment

Gave up

·Per-tenant physical DB isolation (shared, scoped instead)

·Per-tenant custom builds

·Simplest possible routing (middleware adds a hop)

07

Outcome


0

cross-tenant data access by construction

1

deployment serving all tenants

data

onboarding a tenant needs no code change