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. O(1) request lane classification routes health/static/turbo traffic before the full sequence.
-
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.
π¦ Request Lane Router (O(1) pre-pipeline)
Before the READY/SETUP sequence runs, export const handle classifies every request with classifyRequest() (src/hooks/handle-request-classifier.ts) and stamps the response with x-svelty-lane.
| Lane | When | Behavior |
|---|---|---|
FAST_STATIC |
/favicon.ico, /robots.txt, /_app/*, /static/* |
Skip full stack; long-cache headers |
HEALTH |
/health, /api/system/health |
Immediate JSON health (no auth / full pipeline) |
HYPER_TURBO |
Authenticated GET/HEAD on cacheable API prefixes + session cookie | Eligible for handleTurboGet TURBO-HIT via user-scoped responseCache |
API_READ |
Other GET API | Full security + auth path |
API_WRITE |
POST/PUT/PATCH/DELETE | Full CSRF + RBAC + mutation pipeline |
APP_SSR |
Admin pages | Full SSR pipeline |
BOOTSTRAP |
/setup, /login, /auth |
Setup/auth shell |
FILES |
/files/*, /media/* |
File server path |
PUBLIC_SITE |
Non-admin public pages | Public SSR |
Session cookie for turbo detection: auth_sessions (and __Host- / __Secure- variants) β not a legacy svelty_session name. Classifier stores the lane on event.locals.lane.
Operator guide + benchmarks: performance-architecture Β· Tests: unit classifier/cache + handle-turbo-get-lane, integration request-lane-headers, E2E hooks-lane-smoke.
Request β classifyRequest (O(1))
ββ FAST_STATIC / HEALTH β withLane(fast response) [no full sequence]
ββ other lanes β getPipeline() sequence β withLane(response)
ποΈ Middleware Pipeline (2026 Optimized)
The pipeline is dynamically dispatched based on system state and request lane, using pre-compiled, cached sequences:
- API Lanes Pipeline (
API_READ,API_WRITE,HYPER_TURBO- 14 hooks): Bypasses page-only SSR hooks (redirects,aeo-headers,user-preferences) while strictly preserving full security, rate limiting, authentication, authorization, and CSRF protection. - READY / APP_SSR Pipeline (17 hooks): Full production middleware chain for admin and SSR page rendering.
- SETUP Pipeline (2 hooks): Minimal bootstrap chain during initial setup.
// API Lanes Pipeline (API_READ / API_WRITE / HYPER_TURBO):
const apiPipeline = [
handleTurboPipeline, // 1. CONSOLIDATED GATEWAY & Security Headers
handleSecurity, // 2. Firewall, GraphQL Shield, WAF Load Shedding
handleRateLimit, // 3. Per-IP Fixed-Window Rate Limiting (mutations only)
handleSystemState, // 4. AUTHORITATIVE GATEKEEPER
handleTurboGet, // 5. π TURBO GET (Session Cache + responseCache L1 for GETs)
handleCompression, // 6. ESM-Native Streaming (Zstd/Brotli/Gzip)
handleAuthentication, // 7. Identity, Session Token Validation (__Host-auth_sessions) β sets locals.permMask
handleAuthorization, // 8. RBAC: bitmask (Phase 2) + role bitset + legacy fallback, sets locals.roles
handleLocalContext, // 9. Zero-Latency CMS Bridge (LocalCMS) & Content System per Tenant
handleAuditLogging, // 10. Fire-and-Forget Audit Trails (mutations only)
handleApiRequests, // 11. API Caching (ETags, L1 Sync Cache)
handleTokenResolution, // 12. RBAC Token Replacement (reads only)
];
August 2026 Pipeline Hardening & Zero-Overhead Dispatch:
- API Lane Fast-Path: API endpoints automatically bypass SSR-only hooks (
handleRedirects,handleAeoHeaders,handleUserPreferences), saving 3 middleware invocations per API request.- Read vs. Write Execution Parity:
- Read Operations (
findById,find,list): Safe HTTP methods (GET,HEAD,OPTIONS) completely bypass CSRF checks (0Β΅s overhead), leverage synchronous L1 cache hits, and execute direct prepared statements.- Write Operations (
create,update,bulk): Single CSRF verification via double-submitX-CSRF-Tokenheader +__Host-csrf_tokencookie, single schema LRU resolve, single-pass unified validation (prepareWritePayload), and detached non-blocking post-write hooks (schedulePostWrite).- Zero Redundant Checks: No duplicate permission queries, no multiple CSRF evaluations, and no duplicate schema fetches between the gateway and DB adapters.
- Zero-Overhead Dispatch:
wrapHandlepasses raw resolved handlers directly to SvelteKitsequence(), eliminating nested timing closures unlessENABLE_HOOK_TIMING=1is set.- Fast Guest Resolution:
handleAuthenticationdetects credential-free safe reads and resolves guest locals in 0 microseconds without cookie scanning.- Asynchronous Audit Outbox:
handleAuditLoggingenqueues mutation entries into an in-memory ring buffer flushed via microtasks, decoupling response flushes from audit disk writes.
2026-09 Phase 2 β 64-Bit Bitmask Security Engine:
handleAuthenticationnow deserializes the sessionpermMaskhex string tolocals.permMask: bigint(zero allocation βBigInt(hex)) and stores it in the session L0 cache alongsideamr.handleAuthorizationforwardslocals.permMasktosetTurboAuthContextso the turbo fast-path carries the bitmask across request boundaries.- API dispatcher (
+server.ts):_checkEndpointPermissionaccepts an optionalpermMask?: bigint. When present, it callsgetPermissionBit(permission)βhasPermissionBitmask(mask, bit)β evaluated as(mask & bit) !== 0n, a single CPU register operation with zero heap allocations. Falls back to the role-based LRU cache path for unmapped/dynamic permissions.- Cross-worker invalidation:
SharedArrayBuffer+Atomics.compareExchangeversion counter ensures role bitset cache misses cascade to all Node.js worker threads within one event-loop tick.- FLAC reservation: bit 63 is permanently reserved for Field-Level Access Control (FLAC) β
field:*permissions already route to the forward-compatibleevaluateFieldPermissionhandler.
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.
handleTurboPipeline - Consolidated Gateway
First hook. Classification, static delegation, setup-state gating, health fast-path, and the TEST_MODE-only terminal bypass (real session user resolution when a session cookie is present). No benchmark forge β benchmarks authenticate with real sessions like production.
π‘ 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:
* `initHardwareProfile()` detects the machine ONCE (memory the OS actually
* allows, CPUs this process may use) and publishes it to the shared registry;
* everything else reads that object instead of probing again.
*
* `UV_THREADPOOL_SIZE` is NOT set here: libuv reads it before the loop starts,
* so a runtime assignment is a no-op (measured: blocking pbkdf2 n=8/n=4 ratio
* 1.69 set at runtime vs 1.25 pre-set). The entry (`index.cjs`) applies the
* profile's derived value before the server starts.
*/
initHardwareProfile();
/**
* β¨ 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-09-22, bun test tests/benchmarks/hooks-performance.test.ts):
| Scenario | Avg Latency | p95 | RPS |
|---|---|---|---|
| Static Asset (no hooks) | 0.79ms | 1.01ms | 1,240 |
| Turbo Pipeline (Light) | 0.41ms | 0.61ms | 2,332 |
| Full Security + Auth Pipeline | 0.47ms | 0.74ms | 1,985 |
| REST with API Caching (warm GET) | 0.40ms | 0.49ms | 2,479 |
| Mutation (audit logging disabled) | 1.99ms | 2.92ms | 478 |
The measured middleware-only delta (full auth+security lane minus the light turbo lane) is +0.01β0.09 ms (10β90 Β΅s) per request on this machine; everything above that is HTTP framing and response serialization, which is also why a bare static asset is not faster than an API read.
Benchmark:
BENCHMARK_RECORD=1 bun test tests/benchmarks/hooks-performance.test.ts. Rows move with machine load β the deltas between lanes are the signal, not the absolute values.
Auth & Middleware Attribution (2026-09-22)
ENABLE_HOOK_TIMING=1 plus readHookTimings()/hookPhaseCost() (tests/benchmarks/modules/benchmark-utils.ts) attribute a row to named pipeline stages from the serverβs own counters (representative run, 2026-09-22, SQLite; single-run variance on this machine is Β±10%):
| Phase | turbo-pipeline | security | system-state | turbo-get |
|---|---|---|---|---|
| reject | 175 Β΅s | 139 Β΅s | 111 Β΅s | 107 Β΅s |
| auth | 123 Β΅s | 86 Β΅s | 63 Β΅s | 59 Β΅s |
β οΈ Read the columns as different work, and the spans as nested, not per stage.
- Nested:
sequence()composes the pipeline so each outer span contains every downstream hook (turbo-pipelineβ the whole chain,turbo-getβ everything after it). A change in total chain work shows up in every wrapper β the earlier reading of βevery wrapper inflates ~2Γ, including the credential-blind onesβ was that nesting artifact, not evidence of a per-stage cost. - Different work by construction: the
authcolumn is the turbo-cache-served/api/user/merow (4 hooks), while a rejection runs the full chain to reach its 401 (7 hooks) β the harness prints that lane/cache/hook-count per row. Compare the wire numbers, not these spans; a rejection is not the slower path (see below).
Rejection cost parity (resolved 2026-09-22). A 401 used to cost ~2Γ a valid read (every rejected flavour 0.73β0.78 ms vs 0.33β0.36 ms authenticated; two runs each side on the same build). The root cause was not in the middleware: the production 401/403 fast-path envelopes in src/utils/error-handling.ts were the only API responses built without a content-length. handleCompression size-gates on that header, so βunknown sizeβ skipped both the < MIN_COMPRESSION_SIZE (1 KiB) skip-gate and the buffered/sync tier, and negotiated the streaming tier β a fresh createZstdCompress stream per reject (~0.45 ms for a 64-byte body, x-compression-algorithm: zstd + chunked). Accept-Encoding: identity restored the cheap path, which is what isolated it. Declaring the length puts a 64-byte envelope under the 1 KiB gate: reject rows now land at 0.35β0.39 ms, on par with the authenticated rows (0.33β0.36 ms turbo-served, 0.29β0.33 ms full chain).
Row context is part of the measurement now: auth-performance.test.ts prints a live lane/cache/hook-count table per row, because the rows are not equal work β Auth Validation @ 1c (/api/user/me) is served from the turbo cache (X-Cache: TURBO-HIT, 4 hooks), while a rejection must run the full chain to reach its 401 (7 hooks). Auth (full chain) @ 1c is the like-for-like authenticated row (same session cookie, a path the turbo cache never serves, 10 hooks), and the generated ledger picks all of it up on the next matrix run. The snapshot only works when the diagnostics health handler answers: the terminal bypass (handle-turbo-pipeline.server.ts:381/:403) returns a minimal payload without hooks, which the helpers report as unavailable instead of fabricating zeroes.
Response-site audit: content-length is a pipeline contract (2026-09-22)
The rejection fix above generalised: every API response built by hand must declare its length, or handleCompression treats it as βunknown sizeβ and negotiates the streaming tier (fresh zlib/zstd stream per request) instead of skipping (< 1 KiB) or using the buffered sync tier (β€ 64 KiB). Compression is pipeline stage #6, so only responses from hooks downstream of it are compressed at all β handleSecurity, handleRateLimit, handleRedirects and handleTurboPipeline return before it and are never affected:
| Response site | Declares content-length |
Body class | Verdict |
|---|---|---|---|
routes/api/[...path]/handlers/base.ts buildJsonResponse |
β
computed (getFastByteLength) |
0.1β250 KB | already compliant β the standard success path |
β¦/base.ts fastSuccessResponse |
β computed | 0.1β250 KB | already compliant |
utils/error-handling.ts staticJsonEnvelope |
β computed | 64 B | fixed 2026-09-22 (unless rejection-cost parity) |
hooks/handle-api-requests.ts serveCachedEntry (plain branch) |
β computed from the body actually sent | 0.1β250 KB | fixed 2026-09-22 β it used to delete the stale length, so every cache hit was compressed as a stream |
hooks/handle-api-requests.ts serveCachedEntry (compressed branch) |
β absent by design (binary variant; sets Content-Encoding) |
> 1 KB | not applicable β compression early-returns on Content-Encoding |
hooks/handle-local-context.ts 503 envelope |
β computed | 62 B | fixed 2026-09-22 (DB-adapter-unavailable reject) |
hooks/handle-token-resolution.ts rebuilt response |
β computed after rewrite | any JSON | already compliant |
routes/files/[...path]/+server.ts (206 + 200) |
β declared from the file stat | file bytes | already compliant |
routes/api/[...path]/handlers/streaming.ts, content.ts SSE |
β streamed by design | unbounded | text/event-stream is excluded from compression by content type; NDJSON/CSV exports stream |
handleSecurity/handleRateLimit/handleTurboPipeline rejects |
β n/a | < 1 KB | upstream of stage #6 β compression never runs |
Regression guard: tests/unit/hooks/api-requests.test.ts (serveCachedEntry content-length) pins the computed-length contract and the 304 no-body branch; tests/unit/utils/error-handling.test.ts does the same for the error envelopes.
Cold point-read attribution (2026-09-22) β lane refutation
The findByIdRandom competitive row is a different path from the rejects: a warm admin session on GET /api/collections/:collection/:entryId is served by the collection read lane (hooks.server.ts β tryCollectionReadLane), which returns from handle directly β no middleware chain, and therefore no compression stage at all (the response only gets its content-length at socket-write time, when Node writes a single-chunk end()). Verified on-wire: lane HYPER_TURBO, x-cache: MISS, 676 B body + 26 headers (1279 B), content-length: 676.
Update (2026-09-28): the lane now owns its compression instead of skipping it. Inside the buffered budget (β€ 64 KiB) it compresses the negotiated encoding synchronously β the same tier this pipeline uses β and seeds the variant into the same L1 entry; later hits serve stashed zstd/br/gzip variants for free (the response-cacheβs own variant precompute had been commented-out dead code, so no lane entry had ever carried one). Above 64 KiB the lane declines and lets the streaming tier compress while sending: buffering 255 KiB in-lane measured 10.8 ms/request vs 22.7 ms streamed, and the streamed figure is the app-wide tierβs cost β now measurable via api-latency.test.ts instead of skipped (see achievements Β§3.39). Implementation: src/hooks/response-compression-stash.ts.
PROFILE_WRITE=1 spans averaged over one full replica run (n = 6790 misses, SQLite, 10k docs, 8c) attribute the laneβs own work as follows (parent spans include queueing, leaves do not):
| Stage | avg | Note |
|---|---|---|
readLane:sdkFindById (parent) |
127 Β΅s | includes the two rows below plus await resumption |
β³ sdk:crud.findById (adapter) |
55 Β΅s | of which db:get:sql 27 Β΅s (SQLite driver) + db:get:conv 12 Β΅s |
β³ readLane:stringify/cacheSet |
25 Β΅s | one JSON.stringify + one point-cache insert |
readLane:serve (parent) |
69 Β΅s | serveTurboCacheEntry: cached header template β Headers β Response |
readLane:getLocals |
9 Β΅s | LocalCMS.getLocals constructs a fresh SDK facade per request |
readLane:prep + cacheGet |
11β12 Β΅s | session/tenant/context + key build + L1 probe |
The identical sites measured in isolation (Node 24, undici) cost ~9β14 Β΅s for the whole Headers + Response pair, so the in-server leaf numbers are ~4Γ inflated by GC attribution while their relative order holds.
Refutation: no single step in the lane is provably unnecessary work, and none of it is large enough to close a ~70 Β΅s gap. Dedicated spans show what remains: process.cpuUsage-free sampling (Inspector Profiler, 36 k samples over one replica run) puts ~10 % of server CPU in undici web-platform plumbing (header validation/copy + body byte copies), ~4.5 % in all of our bundled chunks combined, ~12 % in the SQLite driver (node:sqlite returns 27 Β΅s where the raw bun:sqlite floor is 3β4 Β΅s), ~8 % in socket writes, ~2 % in GC. The repoβs own raw-HTTP floor (bun run scripts/benchmark-matrix/raw-db-ceiling.ts --db=sqlite --scale=1,10000) is 35.4 k RPS (28 Β΅s) for a findById-shaped bare-node:http + driver + 12 B JSON response, against a 3.2 k RPS (β310 Β΅s) CMS lane row β so the rowβs remaining cost is framework (SvelteKit/adapter-node requestβRequest and Responseβsocket bridging), the SQLite driver and transport, not the laneβs own steps. Deliberately left alone: the point laneβs 2000-entry FIFO (a size/size tradeoff β at 100 k docs it serves ~2 %, at 10 k ~20 %, and enlarging it makes every collection writeβs invalidation scan longer), LocalCMS per-request construction (9 Β΅s, shared-instance caching would put cross-request state in namespaces), and adapter-nodeβs response writer (vendored framework code, patched only for ORIGIN).
Superseded in part (2026-09-24) β three of the βdeliberately left aloneβ items are now settled, with measurements:
LocalCMSper-request construction is removed from the lane. The lane holds one cachedLocalCMSinstance per process instead of agetLocalsfacade; thecollectionsnamespace is stateless per adapter, and tenancy stays request-scoped viaapplyAdapterTenantContext(the same step the lane already ran). Cross-request state never enters a namespace β the namespaces hold no request data, the request state lives onlocals.- The shared-cache L2 probe is gated. With
skipCacheService(the laneβs own call shape)findByIdno longer probescacheService.getSyncβ the lane caches the whole HTTP response inresponseCache, so the probe was a guaranteed miss per cold id. - adapter-node is patched at build time to dispatch the fast-lane registry (
vite.config.tsadapterNodeBuildPatchPlugin), sonode build/index.jsgets the same lane dispatch asindex.server.mjsβ the transport win no longer depends on the entry point, and the vendored response writer stays untouched. Per-requestnew URL()in the lane is replaced by a minimal facade over exactly the five properties the lane reads. Replica at 100 k docs / 8c / PostgreSQL, same host, adapter-node entry:findByIdRandom3 013 β 4 470 β 4 553 RPS,findById9 125 β 13.6 k,listPlain7 817 β ~12 k,findMissing9 363 β ~16 k; create/update/graphql stayed inside the Β±8 % noise band. Byte-identity re-verified viaBENCH_VERIFY_RAW=1on the patched entry.
Point-read payload trimmed + acceptance gate re-scoped (2026-09-24, measured): the point-read HTTP payload dropped its SDK meta (_collection) and platform system columns (tenantId, isDeleted, createdAt, updatedAt, plus null mirror columns) through one shared helper (src/utils/point-read-payload.ts) wired into the laneβs MISS rebuild, the dispatcher fallback AND the predictive turbo stash β so BENCH_VERIFY_RAW=1 byte-parity holds and the probe-shaped body is 1 161 B (~247 B trimmed). The cold split re-measured on the bench-db (Postgres, 360 k rows): lookup 0.010 / db 0.560 / build 0.570 / cachewrite 0.580 / serve 0.630 ms p50 β the app-side miss-path work (lookup+build+cachewrite+serve minus db) is now ~0.07β0.08 ms, under the 0.30 ms gate target in absolute terms; the remaining cold-path cost is the DB read itself (0.56 ms over TCP on the bench host), which the driver-ceiling map already owns. The gate stays: a change that does not move the app-phase sum is not a read-path win.
Phase-mark semantics (2026-10-12): each x-srv-split value is now the phaseβs own duration (a delta). Previously build/cachewrite on the read lane were cumulative from the DB anchor, which the replicaβs phase table (it averages marks directly) read as >900% dominance; lookup/db/build/cachewrite are deltas now, serve/persist alone stay cumulative from request start. The chain recorded above is historical under the old instrumentation.
Machine-noise band (stated honestly): three runs per side of a fingerprint-pinned build (BENCH_DOCS=10000 BENCH_CONCURRENCY=8 bun test tests/benchmarks/competitive-workload-replica.test.ts, SQLite; runs whose build artifact changed mid-flight are discarded) give findByIdRandom warm sequential 0.430 / 0.451 / 0.515 ms before and 0.356 / 0.443 / 0.469 ms after, warm 8c 2.215 / 2.236 / 2.273 ms before and 2.266 / 2.384 / 2.698 ms after β medians 0.451 vs 0.443 ms (seq) and 2.236 vs 2.384 ms (8c), i.e. Β±13β20 % run-to-run noise that swallows the difference in both directions. The single 2.698 ms outlier is the one run taken while a concurrent benchmark was active on the same machine; the same build re-measured 2.266 ms. The two audit fixes do not sit on this rowβs code path by construction (lane-served reads never enter the compression stage, serveCachedEntry and handleLocalContext are pipeline-only), so the pair is a regression check, not an improvement β and it shows no regression.
Write-lane decomposition β x-srv-split sub-phases + x-srv-dur (2026-09-24)
The write laneβs first split (security / persist / serve) proved the security phase costs p50 0.010β0.020 ms per write β but βpersistβ was still a black box containing everything between request.json() and the envelope. The lane now stamps the same x-srv-split header with per-request sub-phase durations handed in via LocalApiOptions.__phaseMarks (internal, absent by default β the performance.now() call sites are skipped entirely when no map is passed):
| Phase | Measured where |
|---|---|
| security | lane: WAF inspect + CSRF + turbo-session + tenant context (cumulative from t0) |
| parse | lane: request.json() + envelope unwrap |
| schema | namespace: peekReadySchema/schemaOf + ensureWidgets + hot flags |
| prep | namespace: prepareWritePayload (sanitize + validate + defaults) + publication gate + beforeSave + widget pipeline |
| encrypt | namespace: encryptWritePayload (no-op when the schema has no encrypted fields) |
| dbwrite | namespace: persistWithOutbox β the adapter INSERT/UPDATE incl. outbox scheduling |
| postwrite | namespace: decryptReadResult + schedulePostWrite + list-warm scheduling |
| persist | lane: the whole SDK create/update call (cumulative from t0) |
| serve | lane: envelope + security headers (cumulative from t0) |
update additionally stamps snapshot (the pre-write revision read β only present when the collection enables revision). SVELTY_SRV_DUR=1 now also stamps total server time (x-srv-dur) on write-lane responses, so hot writes are comparable against the read laneβs acceptance gate. Collect the p50s per phase with tests/benchmarks/probe-write-split.test.ts (see its header for the env recipe).
Reusable CPU sampling for soak runs (2026-09-23)
Attributing a lane needs V8 samples from a replica run, and node --cpu-prof cannot supply them: V8 writes its .cpuprofile only on a clean exit, so a soak or benchmark harness that SIGTERMs its child and SIGKILLs it a moment later leaves no file at all.
SVELTY_CPU_PROFILE=<dir> closes that gap. Sampling starts at boot, and every SVELTY_CPU_PROFILE_INTERVAL_MS (default 30 s, floor 1 s) the profiler stops, writes the snapshot atomically (.tmp + rename, so a hard kill can never leave a truncated file as the newest one), keeps the newest two, and resumes. The profile on disk is therefore at most one interval old regardless of how the process dies. Unset, the switch costs one env read at boot and imports nothing else.
SVELTY_CPU_PROFILE=prof/soak SVELTY_CPU_PROFILE_INTERVAL_MS=5000 bun run preview
# β prof/soak/cpu-<iso stamp>-000.cpuprofile β Chrome DevTools / speedscope
Snapshots are ordinary V8 profiles (nodes, samples, timeDeltas), so the same tooling that reads --cpu-prof output reads them. tests/unit/utils/cpu-profiler.test.ts pins the disabled path, snapshot loadability and the rotation bound.
Competitive Comparison (2026)
Comparative overhead figures must be like-for-like: our own numbers above are HTTP end-to-end per request (0.4β2.0 ms), while the rows below describe middleware-chaining overhead in other stacks. Those are different measurement domains and are not a ranking β treat the table as a pointer to external documentation, not as a measured comparison (see AGENTS.md Β§2b on comparative advertising).
| Stack / Middleware Style | Typical Overhead (per request) | Source |
|---|---|---|
| Minimal Hono / Elysia | 2β12 Β΅s | framework documentation (2026) |
| Fastify (Rate-limit + 4 plugins) | 15β60 Β΅s | framework documentation (2026) |
| Next.js Middleware (Typical) | 40β250 Β΅s | framework documentation (2026) |
| Express (Auth + Security + Logger) | 80β300 Β΅s | framework documentation (2026) |
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 |
security |
~0.05ms | ~0.05ms | Rate limit + firewall (runs before turbo) |
aeo-headers |
~0ms (API) | β | HTML-only: early-returns non-HTML responses (previously ~43Β΅s per API response) |
| 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; >1KB API variants pre-computed (br/gzip/zstd) and stashed into the L1 turbo entry β TURBO-HIT serves the stored variant instead of re-compressing (~17-27Β΅s/KB saved on >4KB payloads) |
| 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 β the single canonical
OPTIONShandler for every/api/route (allowlist validation + CORS + security headers + request-id). The API dispatcher and handlers intentionally have no preflight logic.
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
Classifier: Only meaningful when the request was already tagged HYPER_TURBO (authenticated GET/HEAD on cacheable prefixes).
Purpose: Serves pre-encoded cached API responses BEFORE the auth/authz middleware chain, using a pre-computed session auth context cache + user-scoped responseCache L1 body/ETag.
Architecture:
Request β classifyRequest β β¦ β security β rate-limit β system-state β β‘ TURBO GET β‘
ββ Gate 1: GET/HEAD/OPTIONS only (no mutation bypass)
ββ Gate 2: Cacheable API path? (CACHEABLE_PREFIXES: collections, content, settings, system, graphql, media, dashboard, β¦)
ββ Gate 3: Session cookie (`auth_sessions` / Host|Secure prefix) or Authorization present?
ββ Gate 4: Auth context in turbo cache? (60s TTL, LRU)
ββ Gate 5: Response body in responseCache L1 (user-scoped key)?
ββ HIT β Return pre-encoded response + security headers + x-svelty-lane (skip auth/authz)
ββ 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)
Collection write lane
A warm admin session is served by tryCollectionWriteLane / tryGraphqlWriteLane on the SvelteKit API_WRITE path, before the ten-hook chain. A cold session falls through with the body unread. The socket twin stays off unless SVELTY_FAST_LANE_WRITE=1.
The lane admits POST /api/collections/:id, PATCH/PUT /api/collections/:id/:entryId, and the same collectionβs batch, batch-clone, bulk, increment, and status actions, plus POST /api/graphql. A non-admin on an extended action falls through before the body is read. The lane still applies the operational-state gate, the header and path scan, the declared body cap, CSRF once (GraphQL does not check the token a second time), the user-agent step-up, the admin check, and the mutation limiter. Prefer: return=minimal skips the row read-back on create, update, increment, and status. With no Prefer header the response is the full document. The full-document PostgreSQL update stays dominated by the durable commit; this lane does not change that commit.
Collection Read Lane β bypass policy, gates & the WAF gap
src/hooks/fast-lane.server.ts registers a lane that answers
GET|HEAD /api/collections/:collection[/:entryId] from a warm turbo-auth session before
handleβs pipeline runs. It is not a second implementation: it calls the same
tryCollectionReadLane the pipeline calls, only the transport differs, and BENCH_VERIFY_RAW=1
proves both transports byte-identical in one server run. The read lane is default on
(SVELTY_FAST_LANE=0 opts out); the write lane is opt-in (SVELTY_FAST_LANE_WRITE=1) because it
carries mutation bodies. For the request-class framing β which reads belong to the Wire Plane and
which to the Domain Plane β see Two-Plane Core.
Gates, all evaluated before any cache lookup (so a lane-served body can never outlive a policy change):
| Gate | Rule |
|---|---|
| Operational state | lane-state-gate.ts re-applies handle-system-stateβs predicate (READY/WARMED/WARMING/DEGRADED serve) β consulted by the lane, its peer and the registry dispatch |
| Endpoint parity | collections:read only β exactly what ENDPOINT_PERMISSIONS requires for /api/collections GET |
| Grant freshness | isPermissionBitsetStale(turbo.permRev) declines when a role/permission mutation bumped the global epoch (the turbo mask is a snapshot) |
| FLAC exemption | isLaneFlacExempt() declines when a FIELD_PERMISSIONS policy applies or the schema declares a guarded field (readRoles, writeRoles, requiredAuth, hidden, or permissions.visibility of private or hidden). The lane never reaches handleTokenResolution. |
| Publication clamp | Wire reads pass requirePublished: true for publication-clamped callers; the engine compiles status = 'publish' and fails closed where a table has no status column |
| Wire admission | isWirePlaneAdmissible() β published status (no draft/preview flags or cookies), no populate, default-locale alignment, no mutating afterRead/afterFind hooks, default query shape, fields equal to the compiled projection |
LANE_BYPASSED_HOOKS lists every pipeline hook lane traffic never reaches; a guard test fails when
a hook is added without being classified, so the divergence stays a decision rather than an accident.
Threat scan on the lane β security stays in that list, so lane-served reads do not run heap
shedding, the Redis WAF limiter, or the GraphQL shield. Before any cache or database read the lane
calls wafGuard.inspectEvent (path, query, and headers) and returns the same empty 200 for a
honeypot path or a non-local scanner or AI-crawler user agent. Rate limiting is unchanged: the
API_READ pipeline already skips it for GETs.
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 (IP Flagging): Detects automated reconnaissance bots hitting common shadow routes (e.g.,
/wp-admin,/.env). Returns a decoy 200 OK immediately and flags the IP viasecurityResponseService.blockIp()so the next request is rejected at the firewall layer. (The old 5β15s tarpit was removed β holding sockets open made the honeypot itself a Slowloris/DDoS vector.)
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./api/commerce/*maps to a dedicated WAF limiter (60/min). - Hook lane (
handle-rate-limit.ts): Per-IP + per-tenant fixed window. Guest/api/commercemutations use an isolated commerce lane (tighter cap,X-RateLimit-Lane) so cart floods cannot exhaust admin APIs. - Per-endpoint (e.g., auth actions in
login/+page.server.ts): Critical endpoints apply additionalRateLimiterinstances from@src/hooks/handle-rate-limitfor 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) β For non-setup routes it uses the fast
isSetupComplete(): ifconfig/private.tsis missing, redirects all non-setup routes to/setupwith a 302. For/setuprequests it resolves the DB-aware state (getSetupState()), so the page is only blocked once an admin account + roles exist.config/private.tsis written during the DB test/seed (before the admin is created), so mere file presence must not count as βcompleteβ β the/setuppageβs ownloadlikewise redirects to/loginonly onSetupState.COMPLETE. An abandoned half-setup therefore keeps/setupreachable instead of locking the user out. -
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: Validates language cookies and applies the 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:
- SSR-Safe Language Resolution: Resolves
systemLanguage/contentLanguagecookies onto request-scopedevent.localsβ never a module-global store. (Mutating theappsingleton on the server leaked one requestβs language to every concurrent render in the same process.) - 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. Validate language cookies β request-scoped locals (NO global store mutation)
const systemLangCookie = cookies.get("systemLanguage");
if (systemLangCookie && !isValidLocale(systemLangCookie)) {
cookies.delete("systemLanguage", { path: "/" });
} else {
event.locals.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/handle-authentication.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 || isDemoMode) {
let tenantId: string | null = null;
if (isDemoMode) {
// Demo Mode: one seeded tenant per visitor (DEMO implies tenant isolation)
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/handle-authorization.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.
handleLocalContext - Zero-Latency CMS Bridge & Content Initialization (Pipeline #15)
File: src/hooks/handle-local-context.ts
Service: @src/services/local-cms, @src/content/index.server
Purpose: Injects the LocalCMS SDK into locals.cms and ensures tenant content system initialization in a single unified atomic middleware step.
Key Features:
- Service-Based: Decoupled from the routing layer to prevent circular dependencies.
- 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. - Consolidated Hop: Replaces two separate legacy handles (
handleLocalSdk+handleContentInitialization) with a single async execution, eliminating unnecessary microtask allocations.
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.
applyAllSecurityHeaders - Defense in Depth (Inlined into handleTurboPipeline) {#10-applyallsecurityheaders-defense-in-depth}
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=true) - 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 Turbo as 1. handleTurboPipeline
participant Test as 2. handleTestIsolation
participant Security as 3. handleSecurity
participant RateLimit as 4. handleRateLimit
participant State as 5. handleSystemState
participant TurboGet as 6. handleTurboGet
participant Redirects as 7. handleRedirects
participant Comp as 9. handleCompression
participant Auth as 12. handleAuthentication
participant Authz as 13. handleAuthorization
participant API as 17. 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 / 200 decoy + IP flagged
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->>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 |
| applyAllSecurityHeaders | tests/unit/hooks/security-headers.test.ts | 8+ | Header validation + CSP tests (live inlined function) |
| 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
// getHealthMetrics() β metricsService.getReport()
//
// Turbo-auth revocation (per-user invalidation after role/block/delete mutations)
// invalidateTurboAuthForUser(userId)
Session-cache helpers (invalidateSessionCache, primeSessionMemoryCache) are imported directly from src/hooks/handle-authentication.ts by the auth handler and Auth service β they are not re-exported from hooks.server.ts.
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