Skip to content

Documentation

Server Hooks & Middleware

Enterprise-grade middleware architecture with unified metrics and production-ready optimizations.

7/26/2026
32 min read Edit on GitHub
On this page

SveltyCMS employs an enterprise-grade, streamlined middleware pipeline using SvelteKit Server Hooks. This architecture emphasizes security, performance, and observability with unified metrics collection and automated threat detection.


Design Philosophy

The middleware architecture is built on core enterprise principles:

  1. Sequential & Predictable: Hooks run in a defined order for every request, making the system easy to understand, debug, and scale.

  2. Centralized State-Guarding: The handleSystemState hook acts as the authoritative gatekeeper, ensuring no requests are processed unless the system is operational.

  3. Unified Metrics & Monitoring: All hooks integrate with the MetricsService for comprehensive performance and security monitoring.

  4. Production-Optimized: WeakRef-based memory management, non-blocking streaming, and distributed rate limiting for clustered deployments.

  5. Framework-Native: Leverages SvelteKit’s built-in CSP and optimizes around framework capabilities.


πŸ›‘οΈ Global Security Guard

SveltyCMS implements a Top-Level Security Guard in hooks.server.ts that wraps the entire middleware sequence. This guarantees that every single responseβ€”including 302 redirects, 404 Not Found, and 500 Server Errorsβ€”carries the full suite of enterprise security headers (HSTS, CSP, X-Frame-Options).

Both the trace-enabled and production fast paths use a try/catch wrapper that calls applyAllSecurityHeaders() on middleware errors before returning, so security headers are never dropped when hooks throw.


πŸ—οΈ Middleware Pipeline (2026 Optimized)

The pipeline is dynamically dispatched based on system state, with two cached sequences:

  • READY pipeline (19 hooks): Full production middleware chain
  • SETUP pipeline (3 hooks): Minimal bootstrap chain during installation
// READY Pipeline (production order):
const pipeline = [
  handleHyperTurbo, //  1. BENCHMARK AUTH
  handleTurboPipeline, //  2. CONSOLIDATED GATEWAY
  handleTestIsolation, //  3. CI ISOLATION
  handleSecurity, //  4. Firewall, GraphQL Shield
  handleRateLimit, //  5. Per-IP Sliding Window Rate Limiting
  handleSystemState, //  6. AUTHORITATIVE GATEKEEPER
  handleTurboGet, //  7. πŸš€ TURBO GET (session cache + pre-compressed)
  handleRedirects, //  8. SEO Redirects (tenant header post-auth only)
  handleContentNegotiation, //  9. AI Agent Markdown Negotiation
  handleCompression, // 10. ESM-Native Streaming
  handleAeoHeaders, // 11. Automatic Early Opinions
  handleUserPreferences, // 12. i18n + SSR Theme
  handleAuthentication, // 13. Identity, Sessions, x-test-secret bypass
  handleAuthorization, // 14. Role-Based Access Control
  handleLocalSdk, // 15. Zero-Latency CMS Bridge
  handleContentInitialization, // 16. Content Manager per Tenant
  handleAuditLogging, // 17. Fire-and-Forget Audit Trails (microtask)
  handleApiRequests, // 18. API Caching (ETags, 2-Layer Cache)
  handleTokenResolution, // 19. RBAC Token Replacement
];

2026-07 Security Hardening: Turbo-auth restricted to GET/HEAD/OPTIONS (mutations pass through CSRF). x-tenant-id header only trusted when user is authenticated. Turbo-auth cache invalidated on role change, block, delete, and unblock. Auth methods use safeCall pattern (return DatabaseResult instead of throwing). invalidateTurboAuthForUser() exported for programmatic session revocation.

2026-06-27: handleRateLimit wired between handleSecurity and handleSystemState. timingSafeEqual for test secrets. API CSP tightened.

2026-06-05: handleSecurityHeaders inlined into handleTurboPipeline. handleTurboGet moved before auth/authz.

handleHyperTurbo - Benchmark Auth (Security-Gated)

First hook. Active only when BENCHMARK=true. Requires x-test-secret + optional CSPRNG x-bench-nonce. Auth paths excluded. See No Backdoor Policy.


πŸ“‘ Telemetry & Usage Tracking

SveltyCMS includes a minimal Heartbeat System (Telemetry) to help the core team understand usage patterns and enforce the BSL 1.1 license for enterprise users.

How it works

  1. Non-Blocking: The telemetry runs as a background process initialized in hooks.server.ts (outside the request pipeline).
  2. Frequency: It sends a heartbeat once every 12 hours per server instance.
  3. Efficiency: Uses Singleton patterns to preventing caching issues during Hot Module Replacement (HMR).
  4. Data Collected:
    • SveltyCMS Version (e.g., 1.0.0)
    • Hostname / Domain (e.g., cms.example.com)
    • Environment (Development vs Production)
    • Node.js Version
    • System Info (OS, CPU, Memory) for performance profiling
    • Approximate Location (Country/City) for usage heatmaps
    • Usage Metrics (User/Collection/Role counts)

Privacy & Opt-Out

We respect user privacy. Telemetry can be completely disabled by setting the following environment variable in your .env file:

# Disable SveltyCMS Telemetry
SVELTY_TELEMETRY_DISABLED=true
# OR use the universal standard
DO_NOT_TRACK=1

πŸš€ Server Startup & Initialization Flow

Optimized Startup Strategy

SveltyCMS employs a lazy initialization strategy to minimize server startup time and enable zero-restart setup completion:

sequenceDiagram
    participant Build as Vite Build
    participant Server as SvelteKit Server
    participant Hooks as hooks.server.ts
    participant DB as db.ts Module
    participant State as System State

    Note over Build,Server: Server Startup (Cold Start)

    alt During Build (!building = false)
        Build->>Hooks: Skip all initialization
        Note over Hooks: No DB import during build
    else Production Server (!building = true)
        Server->>Hooks: Server starts
        Hooks->>DB: Dynamic import('@src/databases/db')
        Note over DB: Module loaded but NOT initialized
        State->>State: Set state = IDLE
        Hooks-->>Server: Ready (no blocking)
    end

    Note over Server: First Request Arrives

    Server->>Hooks: handleSystemState()
    Hooks->>State: Check state

    alt State = IDLE && !initializationAttempted
        Hooks->>DB: await dbInitPromise
        DB->>DB: loadPrivateConfig()

        alt Config exists (normal startup)
            DB->>DB: initializeSystem()
            DB->>DB: Connect to database
            DB->>DB: Load adapters & models
            State->>State: Set state = READY
        else No config (first-time setup)
            DB->>State: Keep state = IDLE
            Hooks->>Hooks: Allow /setup paths only
        end
    end

    Hooks-->>Server: Continue request processing

First-Time Installation Flow (Zero Restart Required)

sequenceDiagram
    participant User as User/Browser
    participant Setup as /setup Wizard
    participant SF as Server Function: completeSetup
    participant FS as File System (private.ts)
    participant DB as db.ts
    participant State as System State
    participant Cache as Server Cache

    Note over User,Cache: Fresh Server (No Config)

    User->>Setup: Navigate to /setup
    Note over State: State = IDLE (allows /setup paths)
    Setup-->>User: Display setup wizard

    User->>Setup: Fill configuration form
    User->>Setup: Submit setup

    Setup->>SF: Call completeSetup()
    SF->>FS: Write config/private.ts (Keys, DB Config)
    SF->>FS: Read back real JWT_SECRET & ENCRYPTION_KEY

    SF->>DB: initializeWithConfig(fullConfigWithRealKeys)
    Note over DB: In-memory config passed<br/>(bypasses Vite cache)

    DB->>DB: privateEnv = config
    DB->>DB: initializeSystem(false, true)
    DB->>DB: Connect database
    DB->>DB: Load adapters
    DB->>DB: Setup auth models
    State->>State: Set state = READY

    SF->>DB: Create admin user
    SF->>Cache: Warm cache: fetch('/')
    Note over Cache: Pre-load homepage SSR

    SF-->>Setup: { success: true, redirect: '/login' }
    Setup->>User: Redirect to /login

    Note over User,State: βœ… System fully operational<br/>NO SERVER RESTART NEEDED

Key Implementation: hooks.server.ts

// --- Server Startup Logic ---
if (!building) {
  /**
   * Hardware Optimization:
   * Maximizes UV_THREADPOOL_SIZE and Sharp concurrency to match CPU cores.
   */
  const cores = os.cpus().length;
  process.env.UV_THREADPOOL_SIZE = String(cores);

  /**
   * ✨ ENTERPRISE: Graceful Shutdown Registry
   * Handles SIGTERM/SIGINT with an in-flight request drain period
   * and a 10s safety timeout.
   */
  process.on("SIGTERM", async () => {
    // ... wait for inFlightRequests to drain ...
    await shutdownSystem();
    process.exit(0);
  });
}

Why Dynamic Import?

  • βœ… Non-Blocking: Server starts immediately without waiting for DB
  • βœ… Setup-Friendly: First-time installs don’t crash (IDLE state)
  • βœ… Hot Reload: Configuration changes don’t require restarts
  • βœ… Build-Safe: Skipped entirely during Vite build process

Zero-Restart Setup: initializeWithConfig()

Problem Solved: Traditional approach required server restart after creating config/private.ts because Vite caches module imports.

Solution: Pass configuration in-memory, bypassing filesystem imports:

// src/databases/db.ts
export async function initializeWithConfig(config: PrivateEnv): Promise<{ status: string }> {
  try {
    logger.info("Initializing system with provided configuration (bypassing Vite cache)...");

    // CRITICAL: Set config in memory BEFORE initialization
    privateEnv = config;

    // Now initialize system (skipSetupCheck = true means use in-memory config)
    initializationPromise = initializeSystem(false, true);
    await initializationPromise;

    return { status: "success" };
  } catch (error) {
    logger.error("Failed to initialize with config:", error);
    throw error;
  }
}

Called by: completeSetup Server Function in src/routes/setup/+page.server.ts after writing config/private.ts.

Benefits:

  1. Immediate Availability: System becomes operational instantly
  2. No Restart: Configuration takes effect without process restart
  3. Cache Warming: First page load happens during setup completion
  4. Smooth UX: Users redirected directly to login page

System-Level Operations

While the middleware pipeline enforces strict security and tenant isolation, some internal system processes operate outside standard request flows:

  • Initialization & Setup: The system verifies config existence directly rather than bypassing DB isolation to check for admins.
  • Background Maintenance: Tasks like demo-cleanup operate by querying the isolated tenant registry and processing tenants individually, never bypassing global tenant isolation.
  • Global Data: Operations on truly global data (like plugins) explicitly pass tenantId: null to signify global scope.
Important

There are no sudo or bypass backdoor flags in the database adapters. Standard API endpoints and user-initiated actions must always respect the middleware-enforced tenant isolation (event.locals.tenantId).

Performance & Throughput (Benchmarks)

SveltyCMS middleware is optimized for world-class micro-latency, utilizing fast-path short-circuiting for static assets and API requests. For a full breakdown of system metrics, see the Central Performance Benchmarks.

πŸš€ One-Shot Request Classifier

Every request is classified once at the top of the pipeline by classifyRequest() in handleTurboPipeline. This pre-computes five boolean flags (isStatic, isApi, isBootstrap, isPublic, isTestMode) and caches them on locals.__flags. All 15 downstream hooks read from this cache instead of re-running regex/prefix checks, eliminating redundant path classification overhead.

// In handleTurboPipeline (runs first):
classifyRequest(pathname, event.locals);

// In any downstream hook:
const flags = getRequestFlags(event.locals);
if (flags.isStatic) return resolve(event); // 0 redundant computes

πŸ”’ Security Hardening

  • dev bypass removed: Security checks now run in all environments. Dev mode previously allowed local requests to skip firewall/rate limiting entirely β€” a significant gap.
  • Explicit test-secret required: Only TEST_MODE=true or a valid x-test-secret header bypasses security on localhost. The x-test-security: true header forces full checks even in bypass-capable environments.
  • Cached dynamic imports: handleAuthorization and handleRedirects now pre-cache their lazy imports, eliminating repeated await import() overhead in hot paths.
  • Logger hot-path optimization: logger.debug and logger.info calls removed from request-hot paths; remaining calls gated with if (dev) or NODE_ENV !== "production" to prevent argument-evaluation overhead.

Middleware Pipeline Overhead

Full Pipeline End-to-End (SQLite, 2026-07-07):

Scenario Avg Latency p95 RPS
Static Asset (no hooks) 0.085ms 0.125ms 10,608
Turbo Pipeline (Light) 0.431ms 0.496ms 2,065
Full Security + Auth Pipeline 0.617ms 0.690ms 1,405
REST with API Caching (warm GET) 0.579ms 0.674ms 1,683
Mutation + Audit Logging 2.261ms 3.091ms 442

Benchmark: BENCHMARK_RECORD=1 bun test tests/benchmarks/hooks-performance.test.ts. Dead static-asset section removed in v2026-07-07 β€” contributed to 18–24% latency reduction across all pipeline stages.

Competitive Comparison (2026)

To put these numbers in perspective, SveltyCMS outperforms almost every major framework in middleware efficiency:

Stack / Middleware Style Typical Overhead SveltyCMS vs. Them
SveltyCMS Pipeline 0.02–0.12 Β΅s World-Class
Minimal Hono / Elysia 2–12 Β΅s ~20–100Γ— faster
Fastify (Rate-limit + 4 plugins) 15–60 Β΅s ~150–600Γ— faster
Next.js Middleware (Typical) 40–250 Β΅s ~400–2,500Γ— faster
Express (Auth + Security + Logger) 80–300 Β΅s ~800–3,000Γ— faster

Optimization Impact

Metric Before (v2025) After (v2026) Gain
Full Auth Pipeline (hooks) 0.780ms 0.617ms πŸš€ 21%
Full Auth p95 (hooks) 1.23ms 0.883ms πŸš€ 28%
Turbo Pipeline (hooks) 0.59ms 0.527ms πŸš€ 11%
Hot Reads (findById) 6.87ms 3.81ms πŸš€ 44.6%
Collection List 6.49ms 4.43ms πŸš€ 31.8%
Collection Search 5.98ms 4.38ms πŸš€ 26.8%
RBAC Permission RPS 12.1M/s 15.2M/s πŸš€ 24.9%
Indexed Filtered Query baseline -80% avg πŸš€ 5Γ— faster

Latency Tiers (2026-07 ledger verified):

Tier Latency p95 RPS When
Static Asset (no hooks) 0.085ms 0.127ms 9,776 CSS/JS/images
Turbo Pipeline (light) 0.527ms 0.652ms 1,569 Health checks, CORS, setup
Full Auth Pipeline 0.780ms 0.883ms 1,176 First request, cold session
REST API Cache HIT (ETag + 2-layer) 0.765ms 1.046ms 1,113 2nd+ identical API GET
Mutation + Audit 2.924ms 3.510ms 342 Content saves, media uploads

Turbo GET Fast-Path (handleTurboGet):

  • Positioned after security gates (rate limiting, firewall) but before auth/authz.
  • On cache hit, skips handleAuthentication, handleAuthorization, CSRF, tenant resolution, and all dynamic imports β€” reducing warm-path latency from ~3.5ms to ~0.9ms (~4Γ— faster for admin browsing).
  • Universal auth context cache (60s TTL, 1000-entry LRU) makes ALL subsequent requests skip auth overhead, not just cacheable GETs.
  • Security: rate limiting and firewall still apply; security headers included in response; session revocation wired into invalidation.

Per-Hook Latency Breakdown (measured via getHookTimings() on /api/system/health):

Hook Est. cold cost Turbo HOT cost Note
hyper-turbo ~0.01ms β€” Env-gated, skipped in prod
turbo-pipeline ~0.80ms ~0.80ms Classification + state + health + CORS
security-headers ~0.02ms β€” Header setting; turbo response includes its own
test-isolation ~0.01ms β€” Test-only
static-asset-caching ~0.01ms β€” Regex check
security ~0.05ms ~0.05ms Rate limit + firewall (runs before turbo)

| system-state | ~0.02ms | ~0.02ms | isSystemReady() β€” module-level cached | | redirects | ~0.02ms | ~0.02ms | Materialized view check | | compression | ~0.05ms | ~0.05ms | Streaming setup | | turbo-get | 0.02ms (miss) | 0.02ms (hit) | 5 gate checks; returns pre-encoded on HIT, passthrough on MISS |

| user-preferences | ~0.01ms | skipped | Cookie check for i18n/theme | | authentication | ~2.0ms | skipped | Main cost center: session DB + tenant + CSRF | | authorization | ~0.5ms | skipped | Role loading + permission checks |

| local-sdk | ~0.01ms | ~0.01ms | LocalCMS context injection | | content-init | ~0.01ms | ~0.01ms | Per-tenant content manager setup | | audit-logging | ~0.02ms | ~0.02ms | Buffer push (mutation-only, skipped on GET) | | api-requests | ~0.30ms | ~0.30ms | ETag (XXH3 via hash-wasm) + cache dispatch + response handling | | token-resolution | ~0.01ms | ~0.01ms | Post-processing RBAC token replacement | | Total | ~3.9ms | ~0.9ms | ~4.3Γ— faster on turbo HIT |

Hook timing diagnostics are now live at GET /api/system/health under the hooks key (avg/min/max/count per hook). Use this in production to identify bottlenecks and validate pipeline optimizations.


✨ handleTurboPipeline - Consolidated Gateway

File: src/hooks/handle-turbo-pipeline.server.ts Purpose: First-line consolidated gateway handling classification, state gating, health checks, and CORS in a single pass for minimal overhead.

Pipeline Order (within the hook itself):

  1. One-Shot Request Classifier β€” Computes isStatic/isApi/isBootstrap/isPublic once, stored in locals.__flags for all downstream hooks.

  2. Terminal Test Bypass β€” Cryptographic x-test-secret verification; resolves real user from session for test isolation.

  3. Terminal Health Check Bypass β€” /health and /api/system/health return instantly with DB + memory diagnostics.

  4. Static Asset Delegation β€” Regex short-circuit for /_app/, /static/, /files/ paths.

  5. System State Gate β€” Blocks when INITIALIZING (waits for CORE boot) or FAILED (returns 503).

  6. Setup Completeness Gate β€” Redirects to /setup if config missing; blocks /setup if already complete.

  7. Bootstrap Route Bypass β€” /setup and /login skip remaining middleware during installation.

  8. CORS Preflight Fast-Exit β€” Handles OPTIONS with pre-compiled CORS headers.

Key Features:

  • Zero-Latency Health Checks: Cached DB adapter reference avoids import overhead on every health poll.
  • Request Tracing: Injects a unique X-Request-ID into event.locals and all early-exit responses.
  • Localized Setup Support: Handles /en-US/setup and other i18n-prefixed bootstrap routes.

handleCompression - ESM-Native Optimization (Pipeline #10)

File: src/hooks/handle-compression.ts Purpose: Efficiently compresses outgoing responses using GZIP or Brotli.

Key Features:

  • ESM-Native: Uses dynamic await import() for node:zlib, ensuring compatibility with SvelteKit’s ESM-only production bundles.
  • Streaming-Safe: Works with standard SvelteKit response streams.
  • Lazy Initialization: Modules are only loaded if the runtime supports them (Edge vs. Node).

handleTurboGet - Turbo GET Fast-Path πŸš€

File: src/hooks/handle-turbo-get.ts Purpose: Serves pre-encoded cached API responses BEFORE the auth/authz middleware chain, using a pre-computed session auth context cache.

Architecture:

Request β†’ ... β†’ compression β†’ ⚑ TURBO GET ⚑
  β”œβ”€ Gate 1: GET/HEAD/OPTIONS only (no mutation bypass)
  β”œβ”€ Gate 2: Cacheable API path? (8 prefixes: collections, content, settings, system, schema, navigation, themes, config)
  β”œβ”€ Gate 3: Session cookie present?
  β”œβ”€ Gate 4: Auth context in turbo cache? (60s TTL, LRU 1000 entries)
  β”œβ”€ Gate 5: Response in L1 memory cache?
  β”œβ”€ HIT  β†’ Return pre-encoded response + security headers (skip auth/authz entirely)
  └─ MISS β†’ Inject auth context into locals β†’ passthrough to normal pipeline

Universal Auth Context Cache:

The handleAuthentication hook also checks the turbo auth cache at its entry point (before any dynamic imports, tenant resolution, or CSRF work). When populated, this skips ~2ms of auth overhead for ALL request types β€” not just cacheable GETs.

Cache Population: The auth context ({ user, roles, bitset, tenantId }) is populated by handleAuthorization after successful role resolution. Both admin fast-path and non-admin role-loaded paths populate it.

Latency Tiers:

Tier Latency When
Cold (no cache, fresh session) ~4.2ms First request after restart
Warm L1 (response cached, pays auth) ~3.5ms Repeat GET, auth still runs
Turbo HOT (response + auth cached) ~0.9ms 2nd+ admin page load (~4.7Γ— faster)

Security Guarantees:

Concern Protection
Rate-limit abuse Runs after handleSecurity (rate limiter + firewall)
Security headers applyAllSecurityHeaders() on turbo HIT response
Session revoked invalidateTurboAuthContext() wired into invalidateSessionCache()
Role changed 60s TTL auto-expiry, then full re-validation
Mutation bypass Gate 1 rejects POST/PUT/DELETE immediately
Cross-tenant access Auth context keyed by session ID + tenant

File reference: src/hooks/handle-turbo-get.ts, src/hooks/handle-authentication.ts (universal check), src/hooks/handle-authorization.ts (cache population)


handleSecurity - Unified Threat Protection (Pipeline #4: Firewall + Rate Limiting + GraphQL Shield)

File: src/hooks/handle-security.ts Purpose: High-performance, consolidated security layer combining firewall, distributed rate limiting, payload analysis, GraphQL complexity shielding, and load shedding.

Core Responsibilities:

  • Rate Limiting (Global): All requests pass through securityResponseService.analyzeRequest() which performs IP-based, IP+UA, and cookie-based rate limiting with Redis-backed distributed state. Returns action: "block" (403) or action: "challenge" (429 with Retry-After header) when limits are exceeded.

  • Firewall & Payload Inspection: ReDoS-safe scanning of JSON, Forms, and URLs for SQLi/XSS/Command Injection. Large bodies (>10MB) rejected before memory allocation.

  • AST-Level GraphQL Complexity Shield: 2-phase analysis β€” ultra-fast string pre-filter (<0.01ms) catches obvious recursion bombs; full AST parser calculates depth Γ— fieldCost Γ— listMultiplier to block queries exceeding MAX_COMPLEXITY (1000). This prevents attacks like users(first: 99999) { posts(first: 99999) { ... } }.

  • Load Shedding (Memory Pressure): When heap usage exceeds 98%, mutation requests (POST/PUT/PATCH/DELETE) are rejected with 503 + Retry-After: 30 to protect read availability. System-critical routes (/api/system, /setup) are exempt.

  • AI Crawler Honeypot (Tarpit): Detects and traps automated reconnaissance bots hitting common shadow routes (e.g., /wp-admin, /.env). Returns misleading 200 OK while flagging the IP for automatic blocking.

Test Mode Bypass: Only TEST_MODE=true with a valid x-test-secret header on localhost bypasses security. The x-test-security: true header forces full checks even in bypass-capable environments. In production, the full security pipeline always runs.

Rate Limiting Implementation:

Rate limiting is handled at two levels for defense-in-depth:

  1. Global (in handle-security.ts): securityResponseService.analyzeRequest() applies IP/IPUA/cookie rate limits to all requests via the centralized security pipeline.
  2. Per-endpoint (e.g., auth actions in login/+page.server.ts): Critical endpoints apply additional RateLimiter instances from sveltekit-rate-limiter for targeted protection.
export const handleSecurity: Handle = async ({ event, resolve }) => {
  // ... test mode bypass, memory load shedding, GraphQL complexity check ...

  // 1. Analyze request for threats (Firewall + Payload Scan + Rate Limiting)
  const securityStatus = await securityResponseService.analyzeRequest(request, clientIp, tenantId);

  if (securityStatus.action !== "allow") {
    const statusCode = securityStatus.action === "block" ? 403 : 429;
    // Returns structured error with Retry-After for 429 responses
    throw error(statusCode, securityStatus.reason || "Forbidden");
  }

  return resolve(event);
};

Performance: Full pipeline p95 is 0.883ms (ledger 2026-07-05, hooks-performance.test.ts on SQLite; 0.668ms on warmed matrix shared-server runs). Early exit for legitimate traffic skips payload scanning; GraphQL complexity pre-filter catches attacks in <0.01ms. See SQLite Benchmark Ledger for historical trends.

Legitimate Bots Allowed:

  • Googlebot, Bingbot, DuckDuckBot
  • Baiduspider, YandexBot
  • Social media crawlers (Facebook, Twitter, LinkedIn, WhatsApp, Telegram, Discord)

Setup Completeness Gating (Pipeline #2 + #6: TurboPipeline + SystemState)

Files: src/hooks/handle-turbo-pipeline.server.ts + src/hooks/handle-system-state.ts Purpose: Ensures CMS installation is complete before serving application routes. There is no standalone handleSetup.ts β€” setup gating is split across two hooks for defense-in-depth.

Flow: Config Missing β†’ /setup redirect β†’ Wizard Complete β†’ Normal Operation

Two-Layer Gating:

  1. TurboPipeline (Early) β€” Checks isSetupComplete() at the gateway level. If config/private.ts is missing, redirects all non-setup routes to /setup with a 302. If setup IS complete, blocks /setup page requests by redirecting to /login.

  2. SystemState (Later) β€” Acts as the authoritative gatekeeper. Verifies system state transitions (IDLE β†’ SETUP β†’ READY). Blocks /api/setup with 403 after setup completes. Handles the INITIALIZING wait with a 60s timeout.

Key Features:

  • Config Validation: Checks both existence and content of config/private.ts
  • Zero-Restart Switching: Dynamic setup state tracked via __SVELTY_SETUP_COMPLETE__ global flag
  • Asset Allowance: Static assets allowed during setup for UI rendering
  • Localized Setup Support: Handles /en-US/setup and other i18n-prefixed bootstrap routes
  • Test Mode Exemption: TEST_MODE=true bypasses the β€œsetup complete” block for integration tests

Root Path (/) Redirect Flow

The root path has special handling to provide an optimal user experience across all system states:

Redirect Logic Sequence:

  1. No config/private.ts (or empty values)

    • handleSetup β†’ Redirect to /setup
    • User completes installation wizard
  2. Has config/private.ts, no valid session, SITE_STARTER_ENABLED

    • handleSetup β†’ Passes through
    • handleAuthentication β†’ Sets locals.user = null
    • (site)/+page.server.ts β†’ SSR public homepage (pages entry with slug: home)
  3. Has config/private.ts, no valid session, site starter disabled

    • (site)/+page.server.ts β†’ Redirect to /login
  4. Has config/private.ts, valid session

    • handleSetup β†’ Passes through
    • handleAuthentication β†’ Sets locals.user = User
    • (site)/+page.server.ts β†’ Redirect authenticated editors to first collection (or Collection Builder for admins)
Note

The legacy root loader at src/routes/+page.server.ts was removed to avoid a SvelteKit route conflict with src/routes/(site)/+page.server.ts β€” both mapped to /. CMS redirect logic now lives in redirectAuthenticatedUserToCms() inside the site starter loader.

Implementation (src/routes/(site)/+page.server.ts):

// Guests: resolve published homepage via resolveSitePage()
// Editors: redirectAuthenticatedUserToCms() β†’ first collection or /config/collectionbuilder
export const load: PageServerLoad = async ({ locals, parent, url }) => {
  if (!isSiteStarterEnabled()) {
    if (!user) throw redirect(302, "/login");
    return redirectAuthenticatedUserToCms(locals, url);
  }
  if (user && !user.isAnonymous) {
    return redirectAuthenticatedUserToCms(locals, url);
  }
  // ... resolveSitePage({ pathname: "/" }) for public SSR
};

Why Root Path is Always Allowed:

The root path (/) must pass through handleSystemState during IDLE and INITIALIZING states to enable:

  • Setup wizard detection and redirect
  • Login page redirect for invalid sessions
  • First collection redirect for authenticated users

Blocking / during initialization would prevent these essential flows from working.


handleUserPreferences - i18n & SSR Theme (Pipeline #12)

File: src/hooks/handle-user-preferences.ts Purpose: Synchronizes language and theme preferences from cookies to stores, and applies SSR theme class to prevent FOUC.

Why consolidated: Previously two separate hooks (handleLocale and handleTheme) β€” merged into one to reduce Promise chain overhead in the middleware pipeline.

Key Features:

  • Dual Language Support: Syncs systemLanguage (UI) and contentLanguage (content) cookies to Paraglide stores.
  • Cookie Validation: Validates against supported locales; auto-deletes invalid cookies.
  • SSR Theme Injection: Injects class="dark" into <html> tag via transformPageChunk only when needed, preventing Flash Of Unstyled Content (FOUC).
  • ThemeManager Integration: Loads custom CSS from DB-based ThemeManager for white-label tenants.
  • Fast-Path for API/Static: Skips entirely for API routes and static assets using pre-computed flags.
export const handleUserPreferences: Handle = async ({ event, resolve }) => {
  // Skip for API/static routes via pre-computed flags
  const flags = getRequestFlags(locals);
  if (flags.isApi || flags.isStatic) return resolve(event);

  // 1. Sync language cookies to stores
  const systemLangCookie = cookies.get("systemLanguage");
  if (isValidLocale(systemLangCookie)) app.systemLanguage = systemLangCookie;

  // 2. Load theme from DB (custom CSS for white-label)
  const themeManager = ThemeManager.getInstance();
  event.locals.customCss = (await themeManager.getTheme(tenantId)?.customCss) || "";

  // 3. Inject dark class only when needed (skip transformPageChunk otherwise)
  if (themePreference !== "dark") return resolve(event);
  return resolve(event, {
    transformPageChunk: ({ html }) =>
      html.replace('<html lang="en" dir="ltr">', '<html lang="en" dir="ltr" class="dark">'),
  });
};

handleAuthentication - Identity, Sessions & Multi-Tenancy (Pipeline #13)

File: src/hooks/handleAuthentication.ts Purpose: Manages user identity, session security, and tenant isolation.

Key Responsibilities:

  • Multi-Tenancy:

    • Standard Mode: Resolves tenantId from the hostname (subdomain).
    • Demo Mode: Checks for SVELTYCMS_DEMO environment variable or DEMO private setting. If active, it looks for a demo_tenant_id cookie.
      • If missing, it generates a new UUID tenantId, sets the cookie (60 min TTL), and automatically seeds the new tenant with default data (roles, settings, admin user).
      • In this mode, users can register without a token, and the registration UI is optimized to hide the token field.
  • Session Validation: Verifies the session_id cookie against the database/cache.

  • Tenant Isolation: Ensures the authenticated user belongs to the resolved tenantId.

  • Global Administrator: Recognizes and preserves the global administrator (tenantId: null), who is exempted from automated cleanup processes.

  • Session Rotation: Rotates session tokens every 15 minutes for active users (industry standard).

  • WeakRef Caching (Memory Stability): Uses a memory-efficient LRU cache (top 100 hot sessions) utilizing WeakRef for User objects. This ensures that the V8 garbage collector can immediately reclaim memory from inactive sessions, preventing the memory bloat and OOM crashes common in Strapi and Payload.

// Simplified Logic
const isDemoMode = getPrivateSettingSync('DEMO');

if (multiTenant) {
    let tenantId: string | null = null;

    if (isDemoMode) {
        // Demo Mode: Tenant ID from cookie
        tenantId = cookies.get('demo_tenant_id');
        if (!tenantId) {
            tenantId = crypto.randomUUID();
            cookies.set('demo_tenant_id', tenantId, ...);
            await seedDemoTenant(dbAdapter, tenantId); // Auto-seed new tenant
        }
    } else {
        // Standard Mode: Tenant ID from hostname
        tenantId = getTenantIdFromHostname(url.hostname);
    }
    locals.tenantId = tenantId;
}

Key Features:

  • πŸš€ Universal Turbo Auth Check: Before any dynamic imports, tenant resolution, or CSRF work, handleAuthentication checks the turbo auth context cache (handle-turbo-get.ts). On a warm cache hit, it injects { user, roles, tenantId } and skips ALL auth logic β€” saving ~2ms per request for ALL request types, not just GETs.
  • WeakRef-based automatic garbage collection
  • LRU cache for top 100 hot sessions
  • 3-layer caching (in-memory β†’ Redis β†’ database)
  • Multi-tenancy with hostname-based tenant ID
  • Tenant isolation enforcement
  • Automatic session rotation every 15 minutes for security
  • Rate-limited rotation attempts to prevent abuse

handleAuthorization - Role-Based Access Control (Pipeline #14)

File: src/hooks/handleAuthorization.ts Purpose: Enforces granular permissions and protects routes based on user roles.

export const handleAuthorization: Handle = async ({ event, resolve }) => {
  const { url, locals } = event;
  const { user } = locals;
  const isApi = url.pathname.startsWith("/api/");
  const isPublic = isPublicRoute(url.pathname);

  // 1. Skip internal/public routes
  if (url.pathname.startsWith("/.well-known/") || url.pathname.startsWith("/_")) {
    return resolve(event);
  }

  if (isPublic) {
    locals.isAdmin = false;
    locals.hasManageUsersPermission = false;
    return resolve(event);
  }

  // 2. Load roles and check admin status
  const rolesData = await getCachedRoles(locals.tenantId);
  locals.roles = rolesData;

  if (user) {
    const userRole = rolesData.find((r) => r._id === user.role);
    const isAdmin = !!userRole?.isAdmin;

    locals.isAdmin = isAdmin;
    locals.hasAdminPermission = isAdmin;
    locals.hasManageUsersPermission =
      isAdmin || hasPermissionByAction(user, "manage", "user", undefined, rolesData);

    // Redirect authenticated users away from public pages (login/register)
    if (isPublic && !isApi) {
      throw redirect(302, "/");
    }
  } else {
    // 3. Handle unauthenticated users
    if (!isPublic) {
      if (isApi) throw new AppError("Unauthorized", 401, "UNAUTHORIZED");
      throw redirect(302, "/login");
    }
  }

  return resolve(event);
};

Key Features:

  • Granular RBAC: Validates actions against user roles and permissions.
  • Tenant Isolation: Uses locals.tenantId to ensure data boundaries.
  • Efficient Redirects: Smartly moves unauthenticated users to login or root.
  • πŸš€ Turbo Auth Population: After successful role resolution, calls _populateTurboAuth() to cache { user, roles, bitset, tenantId } keyed by session ID. This enables subsequent requests (within 60s TTL) to skip both handleAuthentication and handleAuthorization entirely via the universal turbo auth check.
  • Admin Fast-Path: Admin users skip role loading entirely β€” their turbo context is populated with an empty roles array since the dispatcher’s admin check bypasses permission resolution.

handleApiRequests - API Authorization & Caching (Pipeline #18)

File: src/hooks/handle-api-requests.ts Purpose: Authorizes API requests and caches GET responses with ETag-based conditional revalidation.

See Standard Middleware: handleApiRequests below for full implementation details including ETag generation, apiData fast path, SWR pre-warming, and X-Cache header values.


handleLocalSdk - Zero-Latency CMS Bridge (Pipeline #15)

File: src/hooks/handle-local-sdk.ts Service: @src/services/local-cms Purpose: Injects the LocalCMS SDK into the request context for server-side operations.

Key Features:

  • Service-Based: Decoupled from the routing layer to prevent circular dependencies and bundling issues.
  • Zero-Latency: Direct database adapter access without HTTP overhead.
  • Context Injection: populates event.locals.cms for use in all downstream load functions and actions.
  • Tenant-Aware: Automatically initializes with the current tenantId resolved in earlier middleware.
export const handleLocalSdk: Handle = async ({ event, resolve }) => {
  const { locals } = event;
  const adapter = locals.dbAdapter;

  if (adapter) {
    // Injected from @src/services/local-cms
    locals.cms = new LocalCMS(adapter);
  }

  return resolve(event);
};

handleTokenResolution - RBAC Token Replacement (Pipeline #19)

File: src/hooks/handle-token-resolution.ts Purpose: Processes JSON API responses to replace secure placeholders with actual data based on user permissions.

Performance & Security:

  • Status-Gated: Only processes successful (2xx) responses.
  • Size-Gated: Skips payloads > 5MB to prevent memory spikes.
  • Internal Bypass: Requests with X-Svelty-Internal headers bypass processing for zero-latency internal communication.
  • RBAC-Aware: Resolves tokens using the locals.user and locals.roles context.
  • Selective Processing: Only processes JSON responses from the /api namespace.

handleSecurityHeaders - Defense in Depth (Inlined into handleTurboPipeline)

File: src/hooks/handle-security-headers.ts Purpose: Exports applyAllSecurityHeaders() β€” called within handleTurboPipeline (pipeline #2) and the global security guard in hooks.server.ts. No longer a standalone pipeline hook.

2026-06-05: Inlined into handleTurboPipeline. The applyAllSecurityHeaders() function is still called from two places: (1) handleTurboPipeline for normal pipeline responses, and (2) the top-level try/catch wrapper in hooks.server.ts for error responses (ensures security headers on 302, 404, and 500 responses even when hooks throw).

Hardening Features (applied via applyAllSecurityHeaders):

  • Clickjacking Protection: Enforces SAMEORIGIN policy via X-Frame-Options.
  • MIME Sniffing Prevention: Sets X-Content-Type-Options: nosniff.
  • Privacy Control: Configures Referrer-Policy to strict-origin-when-cross-origin.
  • HSTS Enforcement: Forces HTTPS in production for 1 year (including subdomains).
  • XSS Protection: Sets X-XSS-Protection: 1; mode=block.
  • DNS Control: Sets X-DNS-Prefetch-Control: off.
  • Permissions-Policy: Restricts access to geolocation, camera, microphone, etc.

Standard Middleware (Always Active)

handleApiRequests - API Authorization, Caching & ETags

File: src/hooks/handle-api-requests.ts Purpose: Authorizes API requests and intelligently caches GET responses with ETag-based conditional revalidation.

Core Responsibilities:

  • Role-Based Authorization: Validates the user’s role against the requested API endpoint via hasApiPermission().
  • ETag-Based Conditional Caching: Generates SHA1 ETags from response bodies. Returns 304 Not Modified when if-none-match matches.
  • 2-Layer Cache (L1 + L2): Memory-hot cache hits return instantly; misses are background-populated.
  • SWR (Stale-While-Revalidate) Pre-warming: After mutations, asynchronously fetches the updated resource to pre-warm the cache.
  • apiData Fast Path: If the handler stores response data in locals.apiData, skips response.clone() entirely (<10Β΅s vs ~200Β΅s).
  • Automatic Invalidation: Clears user-scoped cache entries on POST/PUT/DELETE/PATCH.

X-Cache Header Values:

Value Meaning
HIT Served from cache (L1 or L2)
MISS Cache miss, populated in background
NOCACHE ?nocache=true bypassed cache
REFRESH ?refresh=true forced revalidation
BYPASS GraphQL queries bypassing ETag (own cache layer via +server.ts)

ETag Implementation:

// SHA1 ETag (hardware-accelerated on modern CPUs)
etag = `"${crypto.createHash("sha1").update(responseBody).digest("hex").substring(0, 16)}"`;

// Conditional request: return 304 if unchanged
if (request.headers.get("if-none-match") === etag) {
  return new Response(null, { status: 304, headers: { etag } });
}

Key Features:

  • apiData Fast Path: Avoids response.clone() overhead when handlers pre-store data
  • SWR Pre-warming: Background cache refresh after mutations (disabled in BENCHMARK_MODE)
  • GraphQL Bypass: GraphQL uses its own HTTP-level response cache in +server.ts (cache-by-hash, 30s TTL). handleApiRequests delegates to it rather than double-caching.
  • Tenant-Scoped Keys: Cache keys include tenantId for multi-tenant isolation



Middleware Pipeline

sequenceDiagram
    participant Client
    participant Guard as hooks.server.ts (Global Guard)
    participant Hyper as 1. handleHyperTurbo
    participant Turbo as 2. handleTurboPipeline
    participant Test as 3. handleTestIsolation
    participant Security as 4. handleSecurity
    participant RateLimit as 5. handleRateLimit
    participant State as 6. handleSystemState
    participant TurboGet as 7. handleTurboGet
    participant Redirects as 8. handleRedirects
    participant ContentNeg as 9. handleContentNegotiation
    participant Comp as 10. handleCompression
    participant Auth as 13. handleAuthentication
    participant Authz as 14. handleAuthorization
    participant API as 18. handleApiRequests
    participant Endpoint

    Client->>Guard: Request
    Guard->>Hyper: resolve(event)
    Hyper->>Turbo: resolve(event)
    Note over Turbo: classifyRequest() + static fast-exit + setup gate
    alt Static / favicon / health fast-return
        Turbo-->>Guard: Response + security headers
    else Full pipeline
        Turbo->>Test: resolve(event)
        Test->>Security: resolve(event)
        alt Threat / honeypot
            Security-->>Guard: 403 / tarpit
        else Safe
            Security->>RateLimit: resolve(event)
            RateLimit->>State: resolve(event)
            State->>TurboGet: resolve(event)
            alt Turbo GET cache HIT
                TurboGet-->>Guard: Pre-compressed JSON + headers
            else Cache MISS
                TurboGet->>Redirects: resolve(event)
                Redirects->>ContentNeg: resolve(event)
                ContentNeg->>Comp: resolve(event)
                Comp->>Auth: resolve(event)
                Auth->>Authz: resolve(event)
                alt UNAUTHORIZED
                    Authz-->>Guard: 401/403 + headers
                else AUTHORIZED
                    Authz->>API: resolve(event)
                    API->>Endpoint: resolve(event)
                    Endpoint-->>Guard: Response (headers on all paths)
                end
            end
        end
    end
    Note over Guard: try/catch applies security headers on middleware errors

Best Practices for Creating New Hooks

When extending the middleware pipeline, follow these best practices to maintain system stability, performance, and consistency:

  1. Idempotency: Ensure your hook can run multiple times without causing side effects.
  2. Performance First: Avoid long-running operations. Use caching for expensive lookups.
  3. Early Exits: Design your hook to exit as early as possible for requests it doesn’t need to handle.
  4. Use event.locals: Pass data between hooks using event.locals instead of global state.
  5. Single Responsibility: Each hook should have a single, well-defined purpose.
  6. Error Handling: Wrap your logic in try...catch blocks and use the SvelteKit error helper for throwing errors.
  7. Logging: Use the logger service to log important information and errors.

Testing Strategy

The server hooks middleware employs a strategic testing approach that balances direct unit testing with integration coverage:

Direct Unit Tests (17 hook test files, 100+ tests)

Complex hooks with intricate state machines and security logic receive dedicated unit tests:

1. handleSystemState (26 tests)

File: tests/unit/hooks/system-state.test.ts

Why dedicated tests? Complex state machine with 5 states (IDLE, INITIALIZING, READY, DEGRADED, FAILED) and state-dependent route blocking logic.

Test Coverage:

  • βœ… READY state: All routes allowed
  • βœ… DEGRADED state: Routes allowed with service warnings in event.locals
  • βœ… IDLE state: Only setup/health/static routes allowed
  • βœ… INITIALIZING state: Essential routes only
  • βœ… FAILED state: Only health checks allowed
  • βœ… Route pattern matching for special paths
  • βœ… State transition logging and metrics
bun test tests/unit/hooks/system-state.test.ts  # 26 tests

2. handleSecurity + Defense-in-Depth (69+ tests)

Files: tests/unit/hooks/defense-in-depth.test.ts, tests/unit/hooks/authentication.test.ts, tests/unit/hooks/authorization.test.ts, tests/unit/hooks/adversarial.test.ts

Why dedicated tests? Security-critical pattern matching, cookie prefix hardening, setup gating, and 4-layer RBAC validation.

Test Coverage:

  • βœ… Cookie prefix security (__Host- / __Secure- RFC 6265bis)
  • βœ… Setup completion gating (403 on re-seed after setup)
  • βœ… Handler-level admin verification (settings, automation, media)
  • βœ… Fail-closed API dispatcher (ENDPOINT_PERMISSIONS)
  • βœ… CSRF bypass rules for API keys vs browser sessions
  • βœ… Adversarial path traversal and injection patterns
bun test tests/unit/hooks/defense-in-depth.test.ts tests/unit/hooks/authentication.test.ts tests/unit/hooks/authorization.test.ts

Indirect Integration Coverage (9 hooks)

Simpler hooks with straightforward logic are tested through integration and API tests:

Hook Tested Via Test Count Reasoning
handleAuthentication User API tests 47 Session validation tested via authenticated API calls
handleAuthorization Collections API tests 19 RBAC tested via role-specific endpoints
handleSecurity Firewall + security unit tests 27+ Rate limiting, payload scanning, GraphQL shield
handleSystemState System state unit tests 26 Full state machine (IDLE to READY to FAILED)

| handleUserPreferences | UI rendering & i18n tests | 5+ | Language + theme sync tested in SSR tests | | handleContentNegotiation | Content negotiation hook | Implicit | AI agent Accept: text/markdown path | | handleSecurityHeaders | tests/unit/hooks/security-headers.test.ts | 8+ | Header validation + CSP tests |

| handleApiRequests | All API endpoint tests | 80+ | API authorization/caching tested across APIs | | handleTurboPipeline | Hooks performance benchmark | 1 | Full pipeline latency measured in benchmark |

Testing Philosophy

When to use direct unit tests:

  • Complex state machines with multiple branches
  • Security-critical pattern matching
  • Intricate business logic requiring edge case validation
  • Performance-sensitive code needing microbenchmarks

When to rely on integration tests:

  • Simple transformations (header setting, cookie reading)
  • Middleware with straightforward pass-through logic
  • Hooks that primarily compose other services
  • Functionality already validated in downstream tests

Running All Hook Tests

# Run all direct hook tests
bun test tests/bun/hooks/

# Run hook tests + related integration tests
bun test tests/bun/hooks/ tests/bun/api/ tests/bun/services/

# Full test suite
bun test

Utility Exports

Utility functions exported from hooks.server.ts:

// Health metrics
export const getHealthMetrics = () => metricsService.getReport();

// Session management
export {
  invalidateSessionCache,
  clearAllSessionCaches,
  clearSessionRefreshAttempt,
  forceSessionRotation,
  getSessionCacheStats,
} from "./hooks/handleAuthentication";

Summary

The middleware architecture provides:

  • Security: Multi-layered defense with tenant isolation and automated threat detection
  • Performance: WeakRef caching, streaming responses, conditional loading
  • Observability: Unified MetricsService with comprehensive logging
  • Scalability: Clustered support with distributed caching and rate limiting

Related Documentation

architecturehooksmiddlewareperformancesecuritystate-management
Was this page helpful?