Warum Subdomain-basierte Multi-Tenancy

Lassen Sie uns das aufschlüsseln. Sie haben generell drei Optionen bei der Bewältigung von Multi-Tenant-Routing in SaaS-Apps:

Pattern Beispiel Isolation SEO Benutzererfahrung
Pfad-basiert app.com/tenant-a/dashboard Niedrig Gemeinsame Domain-Autorität Fühlt sich wie eine gemeinsame Plattform an
Subdomain-basiert tenant-a.app.com Mittel Subdomain-Autorität Fühlt sich wie eine dedizierte App an
Custom Domain app.tenant-a.com Hoch Volle Domain-Autorität Fühlt sich vollständig white-labeled an

Warum landen wir oft bei Subdomains? Nun, es ist diese Goldlöckchen-Situation—gerade richtig für 95% der Fälle. Der Tenant erhält seine eigene Branded-URL (acme.yourapp.com), und die Komplexität? Überschaubar. Außerdem sind Sie nicht festgefahren, wenn dieser später ein Upgrade auf eine Custom Domain möchte. Es fühlt sich persönlich genug für den Tenant an und hält Ihren Tech-Stack davon ab, sich in eine Rube-Goldberg-Maschine zu verwandeln.

Aber hier ist der Haken: Beginnen Sie mit Subdomains und bieten Sie diese Custom Domains als Premium-Feature an. Mit Next.js Middleware kann das alles durch eine ordentliche Pipeline fließen. Das ist Effizienz pur, oder?

Wildcard Subdomain Next.js Middleware für Multi-Tenant SaaS

Architektur-Übersicht

Stellen Sie sich jeden Request vor, der wie auf einem Förderband in Ihre App einreist. Das erste, auf das er trifft? Ihre Next.js Middleware. Dieses zuverlässige Stück Code extrahiert den Subdomain, findet heraus, zu welchem Tenant er gehört, und schreibt dann entweder den internen Pfad neu oder kennzeichnet diesen Request mit Headern, die Ihre App nutzen kann. Kinderleicht!

Request: acme.yourapp.com/dashboard
    ↓
Middleware: Extract hostname → resolve tenant → inject headers
    ↓
Rewrite: /dashboard → /[tenant]/dashboard (internal rewrite)
    ↓
Page: Reads tenant from params or headers, fetches tenant-specific data

Was hier wirklich passiert, ist ein Zaubertrick—Middleware-Rewrites sind für Nutzer unsichtbar. Ihr Browser zeigt stolz acme.yourapp.com/dashboard an, während hinter den Kulissen Next.js wirklich zu /acme/dashboard routet.

Verzeichnisstruktur

Hier ein Blick darauf, wie Ihr Projekt aussehen könnte:

├── middleware.ts
├── app/
│   ├── [tenant]/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   ├── dashboard/
│   │   │   └── page.tsx
│   │   └── settings/
│   │       └── page.tsx
│   └── api/
│       └── tenant/
│           └── route.ts
├── lib/
│   ├── tenant.ts
│   └── middleware-utils.ts

Wildcard-DNS einrichten

Bevor Sie eine Zeile Code schreiben, müssen Sie sich mit DNS herumschlagen.

Vercel

Diese ist unkompliziert: Gehen Sie zu Ihren Projekteinstellungen und fügen Sie eine Wildcard-Domain hinzu:

*.yourapp.com
yourapp.com

Und machen Sie sich keine Sorgen, Vercel erledigt die schwere Arbeit mit SSL-Zertifikaten für Sie. Seit 2025 benötigen Sie mindestens einen Pro-Plan (das sind $20/Monat pro Teammate), da Hobby-Pläne Pech haben.

Cloudflare

Cloudflare versteht sich gut mit Wildcard-Routing. Richten Sie einen A-Record wie folgt ein:

Type: A
Name: *
Content: <your-server-ip>
Proxy: Yes (orange cloud)

Und wenn Sie zur Vercel-Familie gehören, ersetzen Sie durch einen CNAME-Record:

Type: CNAME
Name: *
Content: cname.vercel-dns.com
Proxy: DNS only (gray cloud)

Warum grau? Vercel verwaltet SSL, und Cloudflares Proxy funktioniert dort nicht gut. Neutral ist Ihr Freund.

Self-Hosted (Nginx)

server {
    listen 80;
    server_name *.yourapp.com;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Für SSL benötigen Sie ein Wildcard-Zertifikat von Let's Encrypt. Ihre DNS-01-Challenge und das Certbot-Plugin machen das sensibler als es klingt.

Middleware bauen

Okay, Sie möchten Code? Hier ist die Middleware, die im Kampf erprobt ist. Ich erspar Ihnen die Herrlichkeit jedes Semikolons, aber vertrau mir, es ist Gold.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

// Domains that should NOT be treated as tenant subdomains
const RESERVED_SUBDOMAINS = new Set([
  'www',
  'api',
  'admin',
  'app',
  'mail',
  'blog',
  'docs',
  'status',
]);

const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN || 'yourapp.com';

export const config = {
  matcher: [
    /*
     * Match all paths except:
     * - _next/static (static files)
     * - _next/image (image optimization)
     * - favicon.ico
     * - public folder files
     */
    '/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*)',
  ],
};

export default async function middleware(req: NextRequest) {
  const url = req.nextUrl;
  const hostname = req.headers.get('host') || '';
  
  // Remove port for local development
  const currentHost = hostname.replace(/:\d+$/, '');
  
  // Check if this is the root domain or www
  if (
    currentHost === ROOT_DOMAIN ||
    currentHost === `www.${ROOT_DOMAIN}` ||
    currentHost === 'localhost'
  ) {
    // This is the marketing site / landing page
    return NextResponse.next();
  }
  
  // Extract subdomain
  let tenant: string | null = null;
  
  if (currentHost.endsWith(`.${ROOT_DOMAIN}`)) {
    const subdomain = currentHost.replace(`.${ROOT_DOMAIN}`, '');
    
    if (RESERVED_SUBDOMAINS.has(subdomain)) {
      return NextResponse.next();
    }
    
    tenant = subdomain;
  } else {
    // This might be a custom domain
    tenant = await resolveCustomDomain(currentHost);
  }
  
  if (!tenant) {
    // Unknown domain — redirect to main site
    return NextResponse.redirect(new URL(`https://${ROOT_DOMAIN}`));
  }
  
  // Rewrite to tenant-specific path
  const tenantUrl = new URL(`/${tenant}${url.pathname}`, req.url);
  tenantUrl.search = url.search;
  
  const response = NextResponse.rewrite(tenantUrl);
  
  // Inject tenant info as headers for downstream consumption
  response.headers.set('x-tenant-slug', tenant);
  response.headers.set('x-tenant-domain', currentHost);
  
  return response;
}

async function resolveCustomDomain(domain: string): Promise<string | null> {
  try {
    // In production, cache this aggressively
    const res = await fetch(
      `${process.env.INTERNAL_API_URL}/api/domains/resolve?domain=${domain}`,
      {
        headers: { Authorization: `Bearer ${process.env.INTERNAL_API_KEY}` },
        next: { revalidate: 300 },
      }
    );
    
    if (!res.ok) return null;
    
    const data = await res.json();
    return data.tenantSlug || null;
  } catch {
    return null;
  }
}

Das Matcher-Pattern ist alles

Sie möchten Geschwindigkeit? Dieses config.matcher-Regex ist Ihr bester Freund. Ohne es zieht jeder Request Middleware mit sich—statische Assets, Bilder, alles. Und das ist ein Ticket in eine Richtung zu 200ms+ zusätzlicher Latenz. Das will niemand. Schauen Sie sich das an: /((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*). Dies allein hat unsere P95-Latenz um 40% reduziert! Ja, das ist irgendwie ein großes Ding.

Warum Rewrite statt Redirect?

Rewriting macht nicht kaputt, was Nutzer in ihrem Browser sehen—wir sprechen von reibungslosem Segeln. Redirects? Das ist, als würde man Ihren Nutzern sagen, Sie würden sie zum richtigen Ort bringen—eine weitere URL in der Box. Bleiben Sie bei rewrite() für eine dramafrei User Experience.

Wildcard Subdomain Next.js Middleware für Multi-Tenant SaaS - Architektur

Tenant-Auflösungsstrategien

Die obige Middleware zieht Tenants aus Subdomains wie ein Profi. Aber Tenant-Existenz? Das muss bestätigt werden. Verschiedene Ansätze für verschiedene Leute bei Strategien.

Strategie 1: Database Lookup in Middleware

Hier ist der Haken: Next.js Middleware läuft auf der Edge Runtime, was bedeutet, Auf Wiedersehen zu vielen dieser gemütlichen Node.js Komfort-APIs zu sagen.

// Works with Prisma Accelerate or @prisma/client/edge
import { PrismaClient } from '@prisma/client/edge';

const prisma = new PrismaClient();

async function resolveTenant(slug: string) {
  return prisma.tenant.findUnique({
    where: { slug },
    select: { id: true, slug: true, plan: true },
  });
}

Strategie 2: KV Store Lookup

Diese ist mein Favorit. Speichern Sie Ihre Slugs in einem KV-Store wie Vercel KV, Upstash Redis oder Cloudflare KV. Edge-Lookups in 1-5ms bedeuten vernachlässigbare Verzögerung.

import { kv } from '@vercel/kv';

async function resolveTenant(slug: string) {
  const tenant = await kv.get(`tenant:${slug}`);
  return tenant as TenantConfig | null;
}

Strategie 3: Statische Allow-List

Kleine Operation? Statische Allow-Lists können Ihr Freund sein—JSON-Dateien zur Build-Zeit, die Dinge straff halten und null-Netzwerk.

import tenants from './tenants.json';

const tenantMap = new Map(tenants.map(t => [t.slug, t]));

function resolveTenant(slug: string) {
  return tenantMap.get(slug) || null;
}

Neuaufbau mit ISR oder Webhooks, um neue Tenants dynamisch zu handhaben.

Datenbankdesign für Multi-Tenancy

Hier wird es ernst. Zwei große Wege zur Auswahl:

Gemeinsame Datenbank mit Tenant ID

Fügen Sie eine tenantId-Spalte hinzu, als wäre sie im Angebot:

CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_projects_tenant ON projects(tenant_id);

Und ja, PostgreSQL RLS ist Ihre Versicherungspolice:

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON projects
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

Das Leben wird einfacher, wenn Sie den Kontext setzen:

await db.execute(sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`);

Datenbank pro Tenant

Möchten Sie Top-notch-Isolation, die wie ein Handschuh in Compliance-Anforderungen passt? Eine Datenbank pro Tenant ist der Weg. Neons Branching und PlanetScales coole pro-Datenbank-Preise machen das realisierbar, auch für kleine Vermögen in 2025.

Custom Domain Support

Hier treffen sich Deployment und Eitelkeit. Tenants brauchen ihre CNAME-eingetragenen Custom Domains, um Ihren Weg zu zeigen. Und SSL? Jeder angeklickte Link muss mit diesem HTTPS-Glanz funkeln.

Vercel Custom Domains API

Vercels API macht das etwa so schmerzlos wie möglich:

async function addCustomDomain(domain: string, tenantId: string) {
  const response = await fetch(
    `https://api.vercel.com/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.VERCEL_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: domain }),
    }
  );
  
  if (response.ok) {
    // Store the domain-to-tenant mapping
    await db.customDomain.create({
      data: { domain, tenantId, verified: false },
    });
  }
  return response.json();
}

Vercel verwaltet SSL, sobald DNS sich einreiht. Schauen Sie sich ihren Platforms Starter Kit an—es ist eine Modellimplementierung.

Cloudflare für SaaS

Dies ist ideal für die schicken Custom-DNS-Manager, da Cloudflares "SSL für SaaS" SSL-Bereitstellung und Proxies einwirft.

Performance und Caching

Bei der Handhabung von Requests zählt jede Nanosekunde. Der Trick ist, Tenant-Auflösung zu beschleunigen.

Cache Tenant Resolution

Sie sparen den Tag (und den Server), indem Sie Tenant-Lookups cachen:

import { LRUCache } from 'lru-cache';

const tenantCache = new LRUCache<string, TenantConfig>({
  max: 10000,
  ttl: 1000 * 60 * 5, // 5 minutes
});

async function resolveTenantCached(slug: string): Promise<TenantConfig | null> {
  const cached = tenantCache.get(slug);
  if (cached) return cached;
  
  const tenant = await resolveTenantFromDB(slug);
  if (tenant) {
    tenantCache.set(slug, tenant);
  }
  
  return tenant;
}

Denken Sie daran, dass In-Memory-Caches nicht für Edge Runtime sind. Gehen Sie mit etwas wie Upstash Redis—das ist, was sie am besten können.

ISR und Multi-Tenancy

ISR liebt Multi-Tenancy, weil es schlau ist. Jeder Tenant erhält eine zwischengespeicherte Version einmalig aufgrund des Neuschreibens von /dashboard zu /acme/dashboard. Keine zusätzlichen Konfigurationen, genießen Sie einfach die Herrlichkeit von Next.js Magic.

Deployment-Konfigurationen

Entscheidungen, Entscheidungen. Lassen Sie uns Deployment-Optionen vergleichen:

Feature Vercel Pro Cloudflare Pages Self-Hosted (Docker)
Wildcard Domains ✅ (manuell)
Custom Domains API ❌ (manuell)
Edge Middleware ❌ (nur Node)
Auto SSL ⚠️ Let's Encrypt
Preise $20/seat/mo + usage $5/mo + usage $50-200/mo Server
Max Custom Domains 50+ Unbegrenzt Unbegrenzt

Für einen glänzenden und schnellen Startup-Start ist Vercel Pro Ihr Ticket. Aber wenn diese Zahlen klettern—mehr Nutzer, mehr Requests—geben Ihnen Cloudflare Pages oder Self-Hosted-Optionen Flexibilität und Wirtschaftlichkeit.

Wir haben Multi-Tenant-Wunderländer mit jeder Methode navigiert, also wenn Sie über Ihren nächsten Schritt nachdenken, könnten unsere Next.js-Entwicklungsfähigkeiten Ihr Leitfaden sein.

Sicherheitsaspekte

Multi-Tenancy ist super cool, bis es das nicht ist. Jeder Tenant erwartet seine Daten sicher—keine Lecks, keine Ausrutscher.

Tenant-Isolations-Checkliste

  1. Nach Tenant ID filtern: Trauen Sie nicht nur URLs. Back-End-Checks sind wichtig.
  2. PostgreSQL RLS: Es ist, als hätte man 24/7 ein Sicherheitsteam auf Ihrer Datenbank.
  3. Subdomains bereinigen: Lassen Sie sich nicht ablenken—nur [a-z0-9-] erlauben. Vermeiden Sie Subdomain-Übernahmen.
  4. Rate Limit pro Tenant: Dynamische Header für API-Drosseln können Ihren Speck retten.
  5. Protokollieren, prüfen, überprüfen: Jede Write-Operation sollte sagen "Erwischt." Zuversicht weckt Vertrauen.
// Validate tenant slug in middleware
const VALID_SLUG = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;

if (!VALID_SLUG.test(subdomain)) {
  return NextResponse.redirect(new URL(`https://${ROOT_DOMAIN}`));
}

CORS und Multi-Tenancy

Ihre API dient vielen Subdomains, Sie brauchen CORS, um nicht zu tanzen:

// app/api/[...route]/route.ts
export async function GET(req: NextRequest) {
  const origin = req.headers.get('origin') || '';
  const isValidOrigin =
    origin.endsWith(`.${ROOT_DOMAIN}`) || 
    await isCustomDomain(origin.replace('https://', ''));
  
  const headers = new Headers();
  if (isValidOrigin) {
    headers.set('Access-Control-Allow-Origin', origin);
    headers.set('Access-Control-Allow-Credentials', 'true');
  }
  
  // ... rest of handler
}

Testen von Multi-Tenant Middleware lokal

Kopfschmerzen entstehen, wenn lokale Entwicklung Subdomains nicht unterstützt. Hier erträumen Sie wieder.

Option 1: Bearbeiten Sie `/etc/hosts`

# /etc/hosts
127.0.0.1 yourapp.local
127.0.0.1 acme.yourapp.local
127.0.0.1 globex.yourapp.local

Aktualisieren Sie dann Ihre Middleware, um .yourapp.local während der Entwicklung zu erkennen:

const ROOT_DOMAIN = process.env.NODE_ENV === 'development' 
  ? 'yourapp.local:3000' 
  : 'yourapp.com';

Option 2: Verwenden Sie nip.io oder sslip.io

Diese Dienste ziehen magische IP-Zuordnungen:

acme.127.0.0.1.nip.io → 127.0.0.1
globex.127.0.0.1.nip.io → 127.0.0.1

Einfach, erfordert keine Bearbeitung von Hosts.

Option 3: Lokale Tunnel mit benutzerdefinierten Subdomains

Verwenden Sie ngrok (oder ähnliches) für schnelle Tunnel:

ngrok http 3000 --hostname=*.your-dev-domain.ngrok.io

Testen sollte nicht schwieriger sein als Real-World-Szenarien.

Schreiben Sie Integrationstests

import { describe, it, expect } from 'vitest';
import { middleware } from './middleware';
import { NextRequest } from 'next/server';

describe('tenant middleware', () => {
  it('rewrites subdomain requests to tenant path', async () => {
    const req = new NextRequest('https://acme.yourapp.com/dashboard');
    const res = await middleware(req);
    
    expect(res.headers.get('x-middleware-rewrite')).toContain('/acme/dashboard');
    expect(res.headers.get('x-tenant-slug')).toBe('acme');
  });

  it('passes through root domain requests', async () => {
    const req = new NextRequest('https://yourapp.com/');
    const res = await middleware(req);
    
    expect(res.headers.get('x-middleware-rewrite')).toBeNull();
  });

  it('rejects invalid subdomain characters', async () => {
    const req = new NextRequest('https://acme--evil.yourapp.com/');
    const res = await middleware(req);
    
    expect(res.status).toBe(307); // Redirect
  });
});

Denken Sie an das größere Bild? Ob die Wahl zwischen Next.js oder Astro, erwägen Sie, auf unsere Headless-CMS-Layer-Insights-Reise zu hopsen.


Denken Sie daran, Erkunden bedeutet Fragen stellen, Anpassen und Wachsen. Melden Sie sich bei uns für eine Brainstorming-Sitzung oder graben Sie tiefer in unsere Preisgestaltung, wenn Sie eine Partnerschaft auf Top-Tier-Ebene anstreben.