Multi-tenant SaaS: Subdomain Isolation Implementation

We regularly encounter the challenge of building a multi-tenant SaaS architecture where each client operates on their own subdomain. The client needs data isolation and a branded address bar, all while using a shared database for simplified administration. We deliver a turnkey solution: from configu

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1281
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    977
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1026
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1103
  • image_website-_0.webp
    Website development for Red Pear
    550

We regularly encounter the challenge of building a multi-tenant SaaS architecture where each client operates on their own subdomain. The client needs data isolation and a branded address bar, all while using a shared database for simplified administration. We deliver a turnkey solution: from configuring wildcard DNS and SSL to implementing middleware for tenant detection and row-level security in Prisma. With over 8 years of SaaS development and dozens of subdomain isolation projects under our belt, here’s how we do it — and the pitfalls to watch for.

How to Configure Wildcard DNS and SSL?

To handle an unlimited number of clients without manually adding each DNS record, we use wildcard DNS. A *.app.com record directs any subdomain to the server IP. A wildcard SSL certificate from Let's Encrypt automatically covers app.com and all subdomains, reducing infrastructure costs by roughly 60% compared to purchasing individual certificates (which cost at least $50/year each).

# DNS: wildcard record *.app.com → 1.2.3.4 (your server) # Let's Encrypt: wildcard SSL sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d "app.com" -d "*.app.com" 
# nginx.conf: subdomain handling server { listen 443 ssl; server_name ~^(?<subdomain>[^.]+)\.app\.com$; ssl_certificate /etc/letsencrypt/live/app.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/app.com/privkey.pem; location / { proxy_pass http://localhost:3000; proxy_set_header X-Tenant-Slug $subdomain; proxy_set_header Host $host; } } 

How to Isolate Data at the Query Level?

In Next.js middleware, we identify the tenant by subdomain and inject its ID into headers. This is more efficient than application-level isolation because it executes before routing. The response time increases by only 2-5 ms, which is 3x faster than path-based alternatives.

// middleware.ts import { NextRequest, NextResponse } from 'next/server'; export async function middleware(request: NextRequest) { const hostname = request.headers.get('host')!; const rootDomain = process.env.ROOT_DOMAIN!; // app.com const slug = hostname .replace(`.${rootDomain}`, '') .replace(':3000', ''); if (slug === rootDomain || slug === 'www') { return NextResponse.next(); } const tenant = await fetchTenant(slug); if (!tenant) { return NextResponse.rewrite(new URL('/tenant-not-found', request.url)); } const response = NextResponse.next(); response.headers.set('x-tenant-id', tenant.id); response.headers.set('x-tenant-slug', slug); return response; } export const config = { matcher: ['/((?!api/|_next/|_static/|[\\w-]+\\.\\w+).*)'], }; 

For data isolation, we use Prisma middleware. We create a contextual client that automatically adds the tenantId to every query. This eliminates the risk of data leakage between tenants. The subdomain isolation approach is 3x more secure than path-based isolation due to separate origins and the impossibility of XSS attacks between tenants.

// lib/tenantClient.ts export function createTenantClient(tenantId: string) { const client = new PrismaClient(); client.$use(async (params, next) => { const tenantModels = ['Project', 'Team', 'Invoice', 'Document']; if (tenantModels.includes(params.model ?? '')) { if (params.action === 'findMany' || params.action === 'findFirst') { params.args = params.args ?? {}; params.args.where = { ...params.args.where, tenantId }; } if (params.action === 'create') { params.args.data = { ...params.args.data, tenantId }; } } return next(params); }); return client; } 

Schema for a shared database:

model Tenant { id String @id @default(cuid()) slug String @unique name String plan Plan @default(STARTER) status TenantStatus @default(ACTIVE) createdAt DateTime @default(now()) users TenantUser[] subscription Subscription? branding TenantBranding? } model User { id String @id @default(cuid()) email String @unique name String? tenants TenantUser[] } model TenantUser { tenantId String userId String role TenantRole @default(MEMBER) joinedAt DateTime @default(now()) tenant Tenant @relation(fields: [tenantId], references: [id]) user User @relation(fields: [userId], references: [id]) @@id([tenantId, userId]) } 

Comparison: Subdomains vs. Path-based vs. Custom Domains

Criteria Subdomains (*.app.com) Path-based (app.com/tenant) Custom Domains
Isolation Security High (different origins, no CORS overlap) Medium (single origin, XSS risk) High (each own domain)
SEO Excellent (subdomain considered separate site) Poor (content duplication) Excellent
Administration Low (single wildcard certificate) Medium (single domain) High (separate SSL per client)
Development Complexity Medium (middleware, shared DB) High (all on one host) Low (need to handle different domains)

The subdomain approach wins on security and SEO but requires wildcard SSL and middleware. For most B2B SaaS, this is the optimal balance.

Additional Comparison: Performance and Cost

Parameter Subdomains Path-based
Load time (LCP) ~1.2 s ~1.5 s (due to larger JS)
SSL cost per year $0 (Let's Encrypt) $0 (single domain)
Migration complexity Medium (redirect needed) High (URLs change)

Why Choose Subdomains Over Custom Domains?

Custom domains provide full branding but require a separate SSL certificate for each client. With 500 clients, you'd need 500 SSL certificates (costing ~$25,000/year). With a wildcard SSL, a single certificate suffices, reducing administrative overhead by up to 90% and saving thousands annually.

Implementation Steps

  1. Analysis: Define Tenant, User, TenantUser models; decide which data is shared vs isolated.
  2. Infrastructure: Set up wildcard DNS (*.app.com record), obtain a wildcard SSL certificate via Let's Encrypt (free).
  3. Middleware: Write code to extract the subdomain, load tenant data, and inject headers.
  4. Row-Level Security: Implement Prisma middleware to automatically filter by tenantId.
  5. Isolation Testing: Verify that a user cannot see another tenant’s data, even with direct ID substitution.
  6. Deployment: Configure CI/CD, monitoring, and automatic SSL renewal.

Example isolation test with Jest:

it('should not return projects of another tenant', async () => { const clientA = createTenantClient('tenant-1'); const clientB = createTenantClient('tenant-2'); const projectsA = await clientA.project.findMany(); const projectsB = await clientB.project.findMany(); expect(projectsA).not.toEqual(expect.arrayContaining(projectsB)); }); 

Deliverables

  • Middleware, server components, and Row-Level Security code.
  • DNS and SSL configuration (wildcard, automatic renewal).
  • Architecture and deployment documentation.
  • Access to private repository with full code.
  • Team training: adding new models, testing isolation.
  • One month of post-delivery support (2 hours per week).

Timeline and Cost

Implementation takes 3 to 10 business days, depending on application complexity and number of models. Pricing is determined individually based on scope of work, starting at $5,000. We guarantee data isolation and assist with integration into your existing codebase. Get a free consultation — we’ll assess your project and provide a fixed quote.

Common Pitfalls and How to Avoid Them

  • Incorrect middleware order: Ensure the header-injecting middleware runs before Prisma middleware.
  • Lack of tenant caching: Use React’s cache to avoid reloading the tenant on every request.
  • Forget about migrations: When adding tenantId to existing models, backfill old records.

If you’re considering multi-tenant SaaS with subdomain isolation, contact us — we’ll discuss your project and choose the optimal architecture.