Geschützte Routen & Middleware

Routenschutz mit Clerk-Middleware konfigurieren, öffentliche Routen verwalten und API-Endpunkte schützen

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_URL konfigurierten Anmeldeseite weiter.

Öffentliche Routen

Diese Routen sind ohne Authentifizierung zugänglich:
MusterSeiten
/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.txtSEO-Robots-Datei
/sitemap.xmlSEO-Sitemap

Geschützte Routen

Alles, was nicht in der öffentlichen Liste steht, ist geschützt. Dazu gehören:
  • /dashboard und 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

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?
  1. Test-Umgebungen — Wenn NEXT_PUBLIC_CLERK_ENABLED=false, umgeht die Middleware Clerk vollständig. Dynamische Imports verhindern, dass das Clerk-SDK überhaupt geladen wird.
  2. Graceful Fallback — Schlägt der Clerk-Import fehl (fehlende Abhängigkeit, Konfigurationsfehler), fällt die Middleware auf NextResponse.next() zurück, anstatt abzustürzen.
  3. 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.

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:
  1. Umgebung prüfen — Wenn Clerk umgangen wird (Test-Modus), wird der geseedete Test-Benutzer verwendet.
  2. Clerk-Benutzer abrufen — Dynamischer Import von currentUser(), um das Bündeln von Clerk in Tests zu vermeiden.
  3. Datenbankbenutzer auflösen — Datenbankdatensatz anhand von clerkId über das Repository-Muster suchen.
  4. Daten vorladen — TanStack Query nutzen, um Dashboard-Daten (Credits, Billing) serverseitig für sofortige Seitenladezeiten vorzuladen.
  5. Kontext bereitstellen — Kinder mit DbUserProvider umschließen, sodass jede Komponente über useDbUser() 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:
KategorieBenutzer-LimitIP-LimitVerwendung für
api100 Req/Std.200 Req/Std.Allgemeine API-Endpunkte
upload10 Req/Std.20 Req/Std.Datei-Upload-Endpunkte
email5 Req/Std.10 Req/Std.E-Mail-Versand-Endpunkte
contact3 Req/Std.Kontaktformular (nur IP)
payments20 Req/Std.Zahlungs-Endpunkte
webhooks100 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.