Wildcard Subdomain Next.js Middleware für Multi-Tenant SaaS
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?

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.

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
- Nach Tenant ID filtern: Trauen Sie nicht nur URLs. Back-End-Checks sind wichtig.
- PostgreSQL RLS: Es ist, als hätte man 24/7 ein Sicherheitsteam auf Ihrer Datenbank.
- Subdomains bereinigen: Lassen Sie sich nicht ablenken—nur
[a-z0-9-]erlauben. Vermeiden Sie Subdomain-Übernahmen. - Rate Limit pro Tenant: Dynamische Header für API-Drosseln können Ihren Speck retten.
- 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.