Multi-tenant SaaS: Subdomain Isolation Implementation

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

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.

Showing 1 of 1All 2062 services
Multi-tenant SaaS: Subdomain Isolation Implementation
Complex
~2-4 weeks
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1358
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    956
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    947

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.

What Does SaaS Platform Development Involve? Multi-Tenancy, Billing, and Beyond

We know this pain by heart. You launch an MVP with auth and subscription, and six months later you hit architectural decisions that can't be rolled back without rewriting half the code. Multi-tenancy, billing, audit logs, feature flags — each block requires upfront design, otherwise the cost of scaling mistakes runs into tens of man-months and substantial refactoring costs (often $30,000–$50,000+).

Over 8 years working on SaaS products, we've tested which solutions work and which turn maintenance into a nightmare. Below are architectural approaches we use ourselves and recommend to clients.

How we build multi-tenancy: isolation without overhead

The first decision is the data separation scheme. Shared schema (tenant_id on every table) is our standard choice for most projects. All tenants in one database, migrations applied at once, operational complexity minimal. In Laravel we implement it via Global Scope:

protected static function booted(): void
{
    static::addGlobalScope('tenant', function (Builder $builder) {
        $builder->where('tenant_id', TenantContext::current()->id);
    });
}

The global scope is only the first line of defense. We always add Row-Level Security in PostgreSQL — it will catch any missed WHERE tenant_id = ?:

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::uuid);

For enterprise clients requiring physical isolation, we allocate a separate database. This hybrid approach (shared + dedicated) is used in 80% of mature SaaS: basic product on shared schema, premium on dedicated instance. We implement it from the first sprint to avoid rewriting logic later. Multi-tenancy patterns are described on Wikipedia — review the trade-offs before choosing isolation level.

Why Is Billing the Most Underestimated Block?

Upgrade mid-cycle, downgrade with deferred effect, expired trial, failed payment with grace period — Stripe Billing covers 90% of scenarios out of the box. We always process webhooks (customer.subscription.updated, invoice.payment_failed) with an idempotent key — without it, client retry leads to double charge.

For CIS markets — YooKassa or Tinkoff recurring. Their APIs are less convenient but cover 54-FZ requirements.

Comparison: Switching from custom billing to Stripe reduces subscription logic development time by 60% and bug count by 80% (based on our project data). That translates to $15,000–$25,000 savings on a typical SaaS MVP.

Onboarding: how not to lose the user before aha-moment

Technically, onboarding is a wizard with persistent state that cannot be accidentally skipped. Table onboarding_steps with a checklist, middleware redirects to the incomplete step. After completion — a flag in user settings, middleware disabled.

Critical nuance: show real product progress, not abstract steps. "Create your first report" instead of "Complete step 3 of 5." We use drip campaigns via Customer.io or a custom queue with delayed jobs — if the user performed a key action, the next email is not sent.

How to Implement Feature Flags and Access Control?

SaaS with plans requires granular control. Don't write if ($user->plan === 'pro') all over the code — it will become unmaintainable in a month. Instead:

  • Backend: Gate + Policy with checks via features table linked to plans.
  • Frontend: context with flags loaded at app initialization.
  • Open-source tools: Unleash or Growthbook — UI for A/B testing and rollout.

Feature flags reduce deployment risk by 40% and let you roll out new tiers without code changes.

How to Protect API from Aggressive Clients?

Rate limiting is a must for public API. One client can bring down all others. In Laravel we use Redis with sliding window counter:

Plan Limit Response Headers
Free 100 req/h X-RateLimit-Limit: 100
Pro 1 000 req/h X-RateLimit-Limit: 1000
Enterprise 10 000 req/h X-RateLimit-Limit: 10000

Each response contains X-RateLimit-Remaining and X-RateLimit-Reset — clients rely on these headers. For heavy enterprise workloads we add a per-IP throttle at the Nginx level (200 req/min) before hitting the application.

Audit Logs and Monitoring: What, Who, and When?

Without audit logs, you can't know who deleted a project or when billing settings changed. Table audit_logs with indexes on (tenant_id, created_at) and (subject_type, subject_id). In Laravel — Observers on key models.

Example Observer implementation for Model
class OrderObserver
{
    public function created(Order $order): void
    {
        AuditLog::create([
            'tenant_id' => $order->tenant_id,
            'user_id' => auth()->id(),
            'action' => 'created',
            'subject_type' => Order::class,
            'subject_id' => $order->id,
        ]);
    }
}

Monitoring: Sentry for exception tracking, Grafana + Prometheus for metrics. Alerts on error rate > 5% and response time p95 > 2s. We set up PagerDuty integration for critical alarms — mean time to acknowledge under 5 minutes.

Our Team's Experience and Guarantees

Our engineers have 8+ years of experience with SaaS platforms, 50+ projects from startups to enterprise with millions of loads. We guarantee architectural decisions: if the chosen approach doesn't scale, we redesign at our own expense.

Deliverables and Guarantees

  • Architecture documentation: diagrams, ERD, sequence diagrams.
  • CI/CD setup (GitHub Actions / GitLab CI).
  • Access to repository, staging, and production.
  • Team training: 2–3 sessions on code review and runbook.
  • Post-launch support for 1 month.
  • Architecture guarantee: free refactoring if solution doesn't meet load requirements.

Work Process

  1. Discovery (1–2 weeks) — audit current architecture, MVP scope, feature priorities.
  2. Design (1 week) — stack selection, multi-tenancy scheme, billing plan.
  3. Development (4–12 weeks) — 2-week sprints, demo after each.
  4. Testing (1 week) — load tests under target load, security audit.
  5. Deployment and training (1 week) — rollout, monitoring setup, documentation handover.

Timeline Estimates

Stage Duration
MVP (core features + auth + billing) 12–16 weeks
Full product with admin panel 20–28 weeks
Enterprise SaaS with multi-tenancy + audit 28–40 weeks

Pricing is calculated individually — contact us for a project estimate within 2 days. Order turnkey development: from design to deployment with architecture guarantee. Get a consultation on your product architecture — first hour free.