Making Code
Back to blog
Architecture5 min read

How to Build a Multi-Tenant SaaS Application in NestJS Without Duplicating Your Code

If onboarding a new customer requires deploying a new application or duplicating an entire database, your SaaS architecture probably isn't ready to scale. Learn how to implement multi-tenancy in NestJS in a clean and maintainable way.

How to Build a Multi-Tenant SaaS Application in NestJS Without Duplicating Your Code

There comes a moment in every successful SaaS platform when someone asks an uncomfortable question:

How do we onboard a new customer without deploying another application?

At first, everything seems simple.

You have an API.

You have a database.

You have one customer.

The architecture works perfectly.

Then a second customer arrives.

Then a third.

Eventually someone proposes something like:

Customer A -> Database A
 
Customer B -> Database B
 
Customer C -> Database C

At first, it sounds reasonable.

A few months later, every deployment, migration, and schema update must be executed multiple times.

Maintenance starts becoming a nightmare.

The problem is not the number of customers.

The problem is the architecture.

What Does Multi-Tenant Mean?

Multi-tenancy means multiple organizations use the same platform while keeping their data completely isolated.

For example:

Company A
 ├── Users
 ├── Invoices
 └── Inventory
 
Company B
 ├── Users
 ├── Invoices
 └── Inventory

Both companies use exactly the same application.

The difference lies in the data.

Every request must execute within the correct tenant context.

The Most Common Mistake

Many implementations start by adding:

companyId
tenantId
organizationId

to every table.

@Entity('users')
export class UserEntity {
  id: string;
 
  tenantId: string;
 
  email: string;
}

Technically, this works.

Until someone forgets to filter:

WHERE tenant_id = ?

Suddenly one customer can see another customer's information.

That is one of the most expensive mistakes a SaaS platform can make.

The Right Question

We should not ask:

How do we filter tenants?

We should ask:

How do we guarantee that tenant filtering is never forgotten?

That distinction changes everything.

Tenant Resolution

The first thing a multi-tenant application needs is a way to identify who is making the request.

Several strategies exist.

Subdomains

acme.myapp.com
 
globex.myapp.com

Headers

X-Tenant-Id: acme

JWT Claims

{
  "sub": "123",
  "tenantId": "acme"
}

For most modern applications, JWT-based tenant resolution is usually the most practical option.

Creating the Tenant Context

Once the tenant is identified, we need to store that information throughout the request lifecycle.

For example:

export interface TenantContext {
  tenantId: string;
}

Middleware:

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    req['tenantId'] = extractTenant(req);
 
    next();
  }
}

From this point forward, any module can access the current tenant context.

Shared Database vs Dedicated Database

This is one of the most important architectural decisions.

Shared Database

Database
 ├── users
 ├── orders
 └── invoices

Every table contains a tenant identifier.

Advantages:

  • Lower infrastructure cost
  • Easier maintenance
  • Simpler operations

Disadvantages:

  • Lower isolation
  • Higher risk of tenant filtering mistakes

Dedicated Database

Tenant A -> Database A
 
Tenant B -> Database B
 
Tenant C -> Database C

Advantages:

  • Strong isolation
  • Higher security
  • Easier compliance requirements

Disadvantages:

  • Higher operational costs
  • More complex migrations
  • More infrastructure management

The Hybrid Model

Many modern SaaS platforms combine both approaches:

Starter Plan
  -> Shared Database
 
Enterprise Plan
  -> Dedicated Database

This allows organizations to optimize costs while offering premium isolation when necessary.

Dynamic Connection Resolution

When using dedicated databases, the application must resolve the correct connection dynamically.

const connection =
  await tenantConnectionFactory.getConnection(
    tenantId,
  );

From that point on, repositories operate against the correct database.

The business domain never needs to know how the connection was selected.

Multi-Tenancy and Hexagonal Architecture

This is where architecture starts paying off.

Use cases should not know anything about:

  • PostgreSQL
  • MongoDB
  • Tenant Resolution
  • Connection Factories

Application handlers remain simple:

await userRepository.create(user);

Infrastructure decides which database connection to use.

This keeps the domain clean and independent from infrastructure concerns.

Security Considerations

Security is not optional in a multi-tenant system.

Always validate:

  • Tenant from JWT
  • Tenant from the resource
  • Tenant from the database connection

Never trust information coming directly from the client.

What You Gain in Practice

Benefit Impact
Scalability New customers without new applications
Lower Costs Shared infrastructure
Security Strong tenant isolation
Maintainability Fewer deployments
Flexibility Support for multiple pricing plans

Final Thoughts

Many companies think building a SaaS platform is about adding more users.

In reality, it is about managing isolation.

If onboarding a new customer requires creating another application, duplicating repositories, or deploying new services, your architecture is probably not truly multi-tenant.

A proper implementation allows organizations to be onboarded, plans to evolve, infrastructure to scale, and data to remain isolated without changing business logic.

And when the domain stops worrying about tenants, the platform can finally scale the way it was intended to.

Get new posts by email

No spam — just a note when I publish something new on backend, cloud, and architecture.

One email when a post goes live. Unsubscribe anytime.

Related articles