Server Hooks & Middleware
Enterprise-grade middleware architecture with unified metrics and production-ready optimizations.
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:
-
Sequential & Predictable: Hooks run in a defined order for every request, making the system easy to understand, debug, and scale.
-
Centralized State-Guarding: The
handleSystemStatehook acts as the authoritative gatekeeper, ensuring no requests are processed unless the system is operational. -
Unified Metrics & Monitoring: All hooks integrate with the MetricsService for comprehensive performance and security monitoring.
-
Production-Optimized: WeakRef-based memory management, non-blocking streaming, and distributed rate limiting for clustered deployments.
-
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-idheader only trusted when user is authenticated. Turbo-auth cache invalidated on role change, block, delete, and unblock. Auth methods usesafeCallpattern (returnDatabaseResultinstead of throwing).invalidateTurboAuthForUser()exported for programmatic session revocation.
2026-06-27:
handleRateLimitwired betweenhandleSecurityandhandleSystemState.timingSafeEqualfor test secrets. API CSP tightened.
2026-06-05:
handleSecurityHeadersinlined intohandleTurboPipeline.handleTurboGetmoved 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
- Non-Blocking: The telemetry runs as a background process initialized in
hooks.server.ts(outside the request pipeline). - Frequency: It sends a heartbeat once every 12 hours per server instance.
- Efficiency: Uses Singleton patterns to preventing caching issues during Hot Module Replacement (HMR).
- 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:
- Immediate Availability: System becomes operational instantly
- No Restart: Configuration takes effect without process restart
- Cache Warming: First page load happens during setup completion
- 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-cleanupoperate 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: nullto signify global scope.
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
devbypass 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=trueor a validx-test-secretheader bypasses security on localhost. Thex-test-security: trueheader forces full checks even in bypass-capable environments. - Cached dynamic imports:
handleAuthorizationandhandleRedirectsnow pre-cache their lazy imports, eliminating repeatedawait import()overhead in hot paths. - Logger hot-path optimization:
logger.debugandlogger.infocalls removed from request-hot paths; remaining calls gated withif (dev)orNODE_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/healthunder thehookskey (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):
-
One-Shot Request Classifier β Computes
isStatic/isApi/isBootstrap/isPubliconce, stored inlocals.__flagsfor all downstream hooks. -
Terminal Test Bypass β Cryptographic
x-test-secretverification; resolves real user from session for test isolation. -
Terminal Health Check Bypass β
/healthand/api/system/healthreturn instantly with DB + memory diagnostics. -
Static Asset Delegation β Regex short-circuit for
/_app/,/static/,/files/paths. -
System State Gate β Blocks when
INITIALIZING(waits for CORE boot) orFAILED(returns 503). -
Setup Completeness Gate β Redirects to
/setupif config missing; blocks/setupif already complete. -
Bootstrap Route Bypass β
/setupand/loginskip remaining middleware during installation. -
CORS Preflight Fast-Exit β Handles
OPTIONSwith 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-IDintoevent.localsand all early-exit responses. - Localized Setup Support: Handles
/en-US/setupand 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()fornode: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. Returnsaction: "block"(403) oraction: "challenge"(429 withRetry-Afterheader) 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 Γ listMultiplierto block queries exceedingMAX_COMPLEXITY(1000). This prevents attacks likeusers(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: 30to 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:
- Global (in
handle-security.ts):securityResponseService.analyzeRequest()applies IP/IPUA/cookie rate limits to all requests via the centralized security pipeline. - Per-endpoint (e.g., auth actions in
login/+page.server.ts): Critical endpoints apply additionalRateLimiterinstances fromsveltekit-rate-limiterfor 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:
-
TurboPipeline (Early) β Checks
isSetupComplete()at the gateway level. Ifconfig/private.tsis missing, redirects all non-setup routes to/setupwith a 302. If setup IS complete, blocks/setuppage requests by redirecting to/login. -
SystemState (Later) β Acts as the authoritative gatekeeper. Verifies system state transitions (IDLE β SETUP β READY). Blocks
/api/setupwith 403 after setup completes. Handles theINITIALIZINGwait 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/setupand other i18n-prefixed bootstrap routes - Test Mode Exemption:
TEST_MODE=truebypasses 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:
-
No
config/private.ts(or empty values)handleSetupβ Redirect to/setup- User completes installation wizard
-
Has
config/private.ts, no valid session,SITE_STARTER_ENABLEDhandleSetupβ Passes throughhandleAuthenticationβ Setslocals.user = null(site)/+page.server.tsβ SSR public homepage (pagesentry withslug: home)
-
Has
config/private.ts, no valid session, site starter disabled(site)/+page.server.tsβ Redirect to/login
-
Has
config/private.ts, valid sessionhandleSetupβ Passes throughhandleAuthenticationβ Setslocals.user = User(site)/+page.server.tsβ Redirect authenticated editors to first collection (or Collection Builder for admins)
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) andcontentLanguage(content) cookies to Paraglide stores. - Cookie Validation: Validates against supported locales; auto-deletes invalid cookies.
- SSR Theme Injection: Injects
class="dark"into<html>tag viatransformPageChunkonly 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
tenantIdfrom the hostname (subdomain). - Demo Mode: Checks for
SVELTYCMS_DEMOenvironment variable orDEMOprivate setting. If active, it looks for ademo_tenant_idcookie.- 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.
- If missing, it generates a new UUID
- Standard Mode: Resolves
-
Session Validation: Verifies the
session_idcookie 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
WeakReffor 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,
handleAuthenticationchecks 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.tenantIdto 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 bothhandleAuthenticationandhandleAuthorizationentirely 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,
apiDatafast 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.cmsfor use in all downstream load functions and actions. - Tenant-Aware: Automatically initializes with the current
tenantIdresolved 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-Internalheaders bypass processing for zero-latency internal communication. - RBAC-Aware: Resolves tokens using the
locals.userandlocals.rolescontext. - Selective Processing: Only processes JSON responses from the
/apinamespace.
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. TheapplyAllSecurityHeaders()function is still called from two places: (1)handleTurboPipelinefor normal pipeline responses, and (2) the top-leveltry/catchwrapper inhooks.server.tsfor error responses (ensures security headers on 302, 404, and 500 responses even when hooks throw).
Hardening Features (applied via applyAllSecurityHeaders):
- Clickjacking Protection: Enforces
SAMEORIGINpolicy viaX-Frame-Options. - MIME Sniffing Prevention: Sets
X-Content-Type-Options: nosniff. - Privacy Control: Configures
Referrer-Policytostrict-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 Modifiedwhenif-none-matchmatches. - 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.
apiDataFast Path: If the handler stores response data inlocals.apiData, skipsresponse.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:
apiDataFast Path: Avoidsresponse.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
tenantIdfor 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:
- Idempotency: Ensure your hook can run multiple times without causing side effects.
- Performance First: Avoid long-running operations. Use caching for expensive lookups.
- Early Exits: Design your hook to exit as early as possible for requests it doesnβt need to handle.
- Use
event.locals: Pass data between hooks usingevent.localsinstead of global state. - Single Responsibility: Each hook should have a single, well-defined purpose.
- Error Handling: Wrap your logic in
try...catchblocks and use the SvelteKiterrorhelper for throwing errors. - Logging: Use the
loggerservice 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
- Cache System - CacheService implementation details
- Security Plugin - SecurityResponseService integration
- Initialization Workflow - System startup sequence