Kit schützt Routen auf Middleware-Ebene – bevor dein Seiten-Code überhaupt ausgeführt wird. Jede eingehende Anfrage durchläuft
middleware.ts, die bestimmt, ob der Benutzer auf die Route zugreifen darf. Nicht authentifizierte Benutzer, die auf geschützte Routen zugreifen, werden automatisch zur Anmeldeseite weitergeleitet.Diese Seite erklärt, wie die Middleware funktioniert, welche Routen öffentlich sind und wie du eigene hinzufügst.
Wie der Routenschutz funktioniert
Next.js-Middleware läuft auf der Edge Runtime, bevor jede Anfrage eine Seite oder API-Route erreicht. Kits Middleware verwendet Clerks
createRouteMatcher(), um Routen als öffentlich oder geschützt zu klassifizieren:- Öffentliche Routen sind ohne Authentifizierung zugänglich. Der Auth-Status ist trotzdem verfügbar (sodass du z. B. einen „Anmelden"- oder „Zum Dashboard"-Button anzeigen kannst), wird aber nicht vorausgesetzt.
- Geschützte Routen erfordern eine gültige Clerk-Sitzung. Wenn der Benutzer nicht angemeldet ist, leitet Clerk ihn automatisch zur in
NEXT_PUBLIC_CLERK_SIGN_IN_URLkonfigurierten Anmeldeseite weiter.
Kit verwendet Next.js-Routengruppen zur Organisation von Seiten. Die
(auth)-Gruppe enthält Login- und Registrierungsseiten, die (dashboard)-Gruppe enthält geschützte Dashboard-Seiten und die (marketing)-Gruppe enthält öffentliche Marketing-Seiten. Routengruppen (Klammern in Ordnernamen) beeinflussen die Dateiorganisation, aber nicht die URL-Pfade – /dashboard verweist auf apps/boilerplate/src/app/(dashboard)/dashboard/page.tsx.Öffentliche Routen
Diese Routen sind ohne Authentifizierung zugänglich:
| Muster | Seiten |
|---|---|
/login(.*) | Anmeldeseite |
/register(.*) | Registrierungsseite |
/privacy(.*) | Datenschutzerklärung |
/terms(.*) | Nutzungsbedingungen |
/imprint(.*) | Impressum |
/payment/(.*) | Zahlungsverarbeitung |
/email-preview(.*) | E-Mail-Template-Vorschau |
/logout(.*) | Abmeldeseite |
/api/health(.*) | Health-Check-Endpunkt |
/api/pricing(.*) | Pricing-Daten-API |
/api/webhooks/lemonsqueezy(.*) | Zahlungs-Webhooks |
/api/webhooks/resend(.*) | E-Mail-Webhooks |
/api/webhooks/clerk(.*) | Auth-Webhooks |
/robots.txt | SEO-Robots-Datei |
/sitemap.xml | SEO-Sitemap |
Geschützte Routen
Alles, was nicht in der öffentlichen Liste steht, ist geschützt. Dazu gehören:
/dashboardund alle Unterrouten (/dashboard/billing,/dashboard/chat-llm, etc.)/api/*-Endpunkte, die nicht explizit als öffentlich aufgeführt sind- Alle neuen Seiten, die du unter der
(dashboard)-Routengruppe hinzufügst
Kit verwendet eine Deny-by-default-Strategie. Neue Routen sind automatisch geschützt, sofern du sie nicht explizit zur öffentlichen Liste hinzufügst. Das ist sicherer als ein Allow-by-default-Ansatz, bei dem das Vergessen, eine Route zu schützen, sensible Daten offenlegen könnte.
Die Middleware
Die Middleware-Datei behandelt mehrere Belange über die Authentifizierung hinaus. Hier ist die vollständige Initialisierung und das Routen-Matching:
src/middleware.ts — Clerk-Initialisierung und Routen-Matching
async function initClerkMiddleware() {
if (!clerkMiddlewareInstance) {
try {
const { clerkMiddleware, createRouteMatcher } = await import(
'@clerk/nextjs/server'
)
// Define public routes that don't require authentication
const isPublicRoute = createRouteMatcher([
'/login(.*)',
'/register(.*)',
'/privacy(.*)',
'/terms(.*)',
'/imprint(.*)',
'/payment/(.*)',
'/email-preview(.*)',
'/logout(.*)',
'/api/health(.*)',
'/api/pricing(.*)',
'/api/webhooks/lemonsqueezy(.*)',
'/api/webhooks/resend(.*)',
'/api/webhooks/clerk(.*)',
'/robots.txt',
'/sitemap.xml',
])
clerkMiddlewareInstance = clerkMiddleware(
async (auth, req: NextRequest) => {
// Public routes: Auth state is available but not required
// This allows pages to check userId for optional redirects
if (isPublicRoute(req)) {
// Don't protect, but auth state is still set by clerkMiddleware
return
}
// Protect all other routes (dashboard, API endpoints, etc.)
// v6: auth.protect() is now a property, not a method call
await auth.protect()
}
)
} catch (error) {
console.warn('Failed to initialize Clerk middleware:', error)
clerkMiddlewareInstance = (_request: NextRequest) => NextResponse.next()
}
}
return clerkMiddlewareInstance
}
Und hier ist die Haupt-Middleware-Funktion, die die vollständige Request-Pipeline orchestriert:
src/middleware.ts — Haupt-Middleware-Funktion
// Main middleware function with security enhancements
async function middleware(request: NextRequest) {
// Always bypass in test/CI environments
if (isTestEnvironment()) {
return NextResponse.next()
}
try {
// 1. Handle CORS preflight requests first
const corsResponse = corsMiddleware(request)
if (corsResponse) {
// Preflight request handled, return immediately
return corsResponse
}
Dynamisches Laden von Clerk
Die Middleware verwendet dynamische Imports zum Laden des Clerk-SDKs. Das ist ein bewusstes Muster:
typescript
let clerkMiddlewareInstance: any = null
async function initClerkMiddleware() {
if (!clerkMiddlewareInstance) {
const { clerkMiddleware, createRouteMatcher } = await import('@clerk/nextjs/server')
// ... configure and cache
}
return clerkMiddlewareInstance
}
Warum dynamische Imports statt Top-Level-Imports?
- Test-Umgebungen — Wenn
NEXT_PUBLIC_CLERK_ENABLED=false, umgeht die Middleware Clerk vollständig. Dynamische Imports verhindern, dass das Clerk-SDK überhaupt geladen wird. - Graceful Fallback — Schlägt der Clerk-Import fehl (fehlende Abhängigkeit, Konfigurationsfehler), fällt die Middleware auf
NextResponse.next()zurück, anstatt abzustürzen. - Lazy Initialization — Die Clerk-Middleware wird einmal erstellt und gecacht. Nachfolgende Anfragen verwenden dieselbe Instanz wieder.
Routen-Matching mit createRouteMatcher
Clerks
createRouteMatcher() nimmt ein Array von Routenmustern entgegen und gibt eine Funktion zurück, die prüft, ob eine Anfrage passt. Muster verwenden Clerks Pfad-Matching-Syntax:/about(.*)passt auf/about,/about/team,/about/anything/api/webhooks/clerk(.*)passt auf den Webhook-Endpunkt und alle Unterpfade/passt nur auf den exakten Root-Pfad
Der Matcher gibt
true für öffentliche Routen zurück. Alle anderen Routen werden an auth.protect() weitergegeben, das eine Weiterleitung zur Anmeldeseite auslöst, wenn der Benutzer nicht authentifiziert ist.Der Middleware-Stack
Die Middleware wird in einer bestimmten Reihenfolge ausgeführt. Jede Stufe muss abgeschlossen sein, bevor die nächste beginnt:
1
Test-Modus-Prüfung
Wenn
isTestEnvironment() true zurückgibt, gibt die Middleware sofort NextResponse.next() zurück – alle nachfolgenden Prüfungen werden übersprungen. Das stellt sicher, dass Tests ohne Clerk-Overhead laufen.2
CORS-Preflight
CORS-Preflight-Anfragen (
OPTIONS-Methode) werden sofort bearbeitet und zurückgegeben. Für Preflight sind keine Auth-Prüfungen notwendig.3
Clerk-Authentifizierung
Die Clerk-Middleware wird ausgeführt und setzt den Auth-Status auf der Anfrage. Für öffentliche Routen ist der Auth-Status verfügbar, aber optional. Für geschützte Routen erzwingt
auth.protect() die Authentifizierung.4
Sicherheits-Header
Nachdem Clerk die Anfrage verarbeitet hat, werden Sicherheits-Header (CSP, HSTS, X-Frame-Options) auf die Antwort angewendet.
5
CORS-Header
Abschließend werden CORS-Header für Nicht-Preflight-Anfragen zur Antwort hinzugefügt.
Neue Routen hinzufügen
Eine öffentliche Route hinzufügen
Um eine neue Route öffentlich zugänglich zu machen:
1
Muster zu createRouteMatcher hinzufügen
Öffne
apps/boilerplate/src/middleware.ts und füge dein Routenmuster zum createRouteMatcher-Array hinzu:typescript
const isPublicRoute = createRouteMatcher([
'/',
'/about(.*)',
// ... existing routes
'/your-new-page(.*)', // Add your route here
])
2
Die Seite erstellen
Erstelle deine Seiten-Datei. Öffentliche Seiten gehören typischerweise in die
(marketing)-Routengruppe:apps/boilerplate/src/app/(marketing)/your-new-page/page.tsx
3
Testen
Rufe die Seite in deinem Browser auf, während du abgemeldet bist. Sie sollte laden, ohne zur Login-Seite weiterzuleiten. Rufe sie eingeloggt auf – der Auth-Status sollte über
useConditionalAuth() verfügbar sein, wenn du ihn benötigst.Eine geschützte API-Route hinzufügen
Geschützte API-Routen erfordern Authentifizierung. Da sie standardmäßig geschützt sind, musst du lediglich den Benutzer im Handler verifizieren:
typescript
// src/app/api/my-endpoint/route.ts
import { getServerAuth } from '@/lib/auth/server-helpers'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { userId } = await getServerAuth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Your protected logic here
return NextResponse.json({ message: 'Hello', userId })
}
Die Middleware stellt sicher, dass der Benutzer eine gültige Sitzung hat, bevor der Handler ausgeführt wird. Die
getServerAuth()-Prüfung ist eine zusätzliche Sicherheitsschicht – falls die Middleware falsch konfiguriert ist, lehnt die API-Route nicht authentifizierte Anfragen trotzdem ab.Für rate-limitierte API-Routen umschließe deinen Handler mit
withRateLimit() aus @/lib/security/rate-limit-middleware. Das kombiniert Auth-Schutz mit nutzerbasierter Anfragebegrenzung. Weitere Details im Abschnitt Sicherheit.Server Component-Authentifizierung
Im Dashboard müssen Server Components wissen, wer der aktuelle Benutzer ist, um Daten abzurufen und Berechtigungen zu prüfen. Kit kümmert sich darum im Dashboard-Layout.
Dashboard-Layout
Das Dashboard-Layout ist eine Server Component, die den aktuellen Benutzer auflöst und seine Datenbank-ID allen Kind-Komponenten bereitstellt:
src/app/(dashboard)/layout.tsx
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const layoutStart = performance.now()
console.log('[PERF] Dashboard Layout start')
let userId: string | null = null
// Only attempt Clerk calls if not in test/CI environment
if (!shouldBypassClerk()) {
try {
const clerkImportStart = performance.now()
// Dynamic import to avoid bundling Clerk in test environments
const { currentUser } = await import('@clerk/nextjs/server')
console.log(
'[PERF] Clerk import took:',
(performance.now() - clerkImportStart).toFixed(2),
'ms'
)
const clerkUserStart = performance.now()
const user = await currentUser()
console.log(
'[PERF] Clerk currentUser() took:',
(performance.now() - clerkUserStart).toFixed(2),
'ms'
)
// Get database user ID for prefetching
if (user?.id) {
const dbUserStart = performance.now()
const dbUser = await userRepository.findByClerkId(user.id)
console.log(
'[PERF] DB user lookup took:',
(performance.now() - dbUserStart).toFixed(2),
'ms'
)
userId = dbUser?.id || null
}
} catch (error) {
// Only log in development to keep build output clean
if (process.env.NODE_ENV === 'development') {
console.warn('Could not fetch current user, using demo mode:', error)
}
}
} else {
// Test mode: Use test user
const testUserStart = performance.now()
const dbUser = await userRepository.findByClerkId(testUser.id)
console.log(
'[PERF] Test user lookup took:',
(performance.now() - testUserStart).toFixed(2),
'ms'
)
userId = dbUser?.id || null
}
Der Ablauf funktioniert so:
- Umgebung prüfen — Wenn Clerk umgangen wird (Test-Modus), wird der geseedete Test-Benutzer verwendet.
- Clerk-Benutzer abrufen — Dynamischer Import von
currentUser(), um das Bündeln von Clerk in Tests zu vermeiden. - Datenbankbenutzer auflösen — Datenbankdatensatz anhand von
clerkIdüber das Repository-Muster suchen. - Daten vorladen — TanStack Query nutzen, um Dashboard-Daten (Credits, Billing) serverseitig für sofortige Seitenladezeiten vorzuladen.
- Kontext bereitstellen — Kinder mit
DbUserProviderumschließen, sodass jede Komponente überuseDbUser()auf die Datenbankbenutzer-ID zugreifen kann.
Durch dieses Muster müssen deine Dashboard-Seiten den aktuellen Benutzer nie selbst abrufen – das Layout hat ihn bereits aufgelöst.
API-Routen-Schutz
Kit bietet zwei Schutzschichten für API-Routen:
Schicht 1: Middleware — Lehnt nicht authentifizierte Anfragen ab, bevor sie den Handler erreichen. Das ist automatisch für alle Routen aktiv, die nicht in der öffentlichen Liste stehen.
Schicht 2: Handler-Level-Auth — Verwende
getServerAuth() im Handler als zusätzliche Prüfung. Das schützt vor Middleware-Fehlkonfigurationen und liefert die userId für Datenbankabfragen.Schicht 3: Rate Limiting — Umschließe Handler mit
withRateLimit(), um pro-Benutzer-Anfragelimits hinzuzufügen. Das kombiniert sich mit Auth, um sowohl nicht authentifizierten als auch übermäßigen Zugriff zu verhindern.typescript
import { withRateLimit } from '@/lib/security/rate-limit-middleware'
export const POST = withRateLimit('api', async (request: NextRequest) => {
// Auth is already verified by middleware + rate limiter
// Your handler logic here
})
Rate-Limit-Kategorien:
| Kategorie | Benutzer-Limit | IP-Limit | Verwendung für |
|---|---|---|---|
api | 100 Req/Std. | 200 Req/Std. | Allgemeine API-Endpunkte |
upload | 10 Req/Std. | 20 Req/Std. | Datei-Upload-Endpunkte |
email | 5 Req/Std. | 10 Req/Std. | E-Mail-Versand-Endpunkte |
contact | — | 3 Req/Std. | Kontaktformular (nur IP) |
payments | 20 Req/Std. | — | Zahlungs-Endpunkte |
webhooks | — | 100 Req/Std. | Externe Webhook-Empfänger |
AI-Endpunkte haben einen separaten tier-basierten Rate-Limiter mit monatlichen Kontingenten (Free: 500/Monat, Basic: 1.500, Pro: 5.000, Enterprise: 15.000) sowie ein globales Burst-Limit von 10 Anfragen pro 10 Sekunden. Weitere Details zur vollständigen Rate-Limiting-Architektur unter Caching & Redis.