Server-Only Layout Data Loading
The +layout.server.js file (or +layout.server.ts) provides server-only data loading for layouts. This is the most common choice for the root layout because it’s where you typically handle authentication — checking if a user is logged in and making that information available throughout your app.
Understanding why this file is so central requires understanding SvelteKit’s request pipeline. Every HTTP request passes through hooks.server.ts before reaching any route. Hooks can read session cookies, validate tokens, and store the result in event.locals — a request-scoped object that travels with the request from hooks all the way through to load functions. The root +layout.server.js is then the ideal place to read from locals and broadcast the user object to every page in your application.
This three-stage pipeline is what makes authentication feel effortless in SvelteKit:
hooks.server.ts → validates the session cookie → sets event.locals.user
+layout.server.ts → reads event.locals.user → returns { user } to all pages
+layout.svelte → receives data.user → renders login/logout UI Server-Only Layout DataThe root
+layout.server.jsruns on every request, making it the perfect place for authentication checks, feature flag loading, and establishing the user context. Get this file right and authentication flows naturally through your entire app without any per-page boilerplate.
The Authentication Pattern
The most important use case for +layout.server.js is authentication. By loading user data in the root layout, every page in your app has access to the current user.
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ cookies, locals }) => {
// `locals` is typically populated by hooks.server.ts
// It contains data like the authenticated user
return {
user: locals.user ?? null
}
} Now every page and layout in your app can access data.user:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { LayoutProps } from './$types'
let { data, children }: LayoutProps = $props()
</script>
<header>
{#if data.user}
<span>Welcome, {data.user.name}</span>
<a href="/dashboard">Dashboard</a>
<form method="POST" action="/logout">
<button>Logout</button>
</form>
{:else}
<a href="/login">Login</a>
<a href="/signup">Sign Up</a>
{/if}
</header>
<main>
{@render children()}
</main> Working with Hooks
Let’s walk through the full pipeline so the pieces connect. The locals object is populated in your hooks.server.ts file before any load function runs:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit'
import { db } from '$lib/server/database'
export const handle: Handle = async ({ event, resolve }) => {
// Read session cookie
const sessionId = event.cookies.get('session')
if (sessionId) {
// Validate session and get user
const session = await db.sessions.findUnique({
where: { id: sessionId },
include: { user: true }
})
if (session && session.expiresAt > new Date()) {
event.locals.user = {
id: session.user.id,
name: session.user.name,
email: session.user.email,
role: session.user.role
}
}
}
return resolve(event)
} // src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals }) => {
// User is already validated in hooks
return {
user: locals.user ?? null
}
} Server-Only Parameters
Like +page.server.js, the layout server load function has access to server-only parameters:
export const load: LayoutServerLoad = async ({
cookies, // Read and write cookies
locals, // Data from hooks.server.ts
request, // The original Request object
url, // The URL object
fetch, // Server-side fetch
platform, // Platform-specific context
setHeaders // Set response headers
}) => {
// Your server-only logic
} Reading Cookies Directly
Sometimes you need to read cookies directly in the layout:
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ cookies }) => {
const theme = cookies.get('theme') ?? 'light'
const locale = cookies.get('locale') ?? 'en'
return {
preferences: {
theme,
locale
}
}
} Loading App-Wide Data
The root layout is perfect for loading data needed everywhere:
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
import { db } from '$lib/server/database'
import { ANALYTICS_KEY } from '$env/static/private'
export const load: LayoutServerLoad = async ({ locals, cookies }) => {
// Get user from hooks
const user = locals.user
// Load user-specific data if logged in
let notifications = []
let unreadCount = 0
if (user) {
const result = await db.notifications.findMany({
where: { userId: user.id },
orderBy: { createdAt: 'desc' },
take: 5
})
notifications = result
unreadCount = await db.notifications.count({
where: { userId: user.id, read: false }
})
}
// Load site-wide settings
const settings = await db.settings.findFirst()
return {
user,
notifications,
unreadCount,
settings: {
siteName: settings?.siteName ?? 'My App',
maintenanceMode: settings?.maintenanceMode ?? false
}
}
} Protected Routes Pattern
Use layout server data to protect entire sections of your app:
// src/routes/(protected)/+layout.server.ts
import { redirect } from '@sveltejs/kit'
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals, url }) => {
if (!locals.user) {
// Redirect to login with return URL
const returnTo = encodeURIComponent(url.pathname)
redirect(303, `/login?returnTo=${returnTo}`)
}
return {
user: locals.user
}
} Now any page inside (protected)/ requires authentication:
src/routes/
├── (protected)/
│ ├── +layout.server.ts → Requires auth
│ ├── dashboard/
│ │ └── +page.svelte → Protected
│ ├── settings/
│ │ └── +page.svelte → Protected
│ └── profile/
│ └── +page.svelte → Protected
├── login/
│ └── +page.svelte → Public
└── +layout.server.ts → Root (loads user if present) Role-Based Access Control
Extend the pattern for role-based permissions:
// src/routes/(admin)/+layout.server.ts
import { error, redirect } from '@sveltejs/kit'
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
redirect(303, '/login')
}
if (locals.user.role !== 'admin') {
error(403, {
message: 'Access Denied',
hint: 'You need admin privileges to access this area.'
})
}
return {
user: locals.user
}
} Data Inheritance
Data from parent layouts is available to child layouts and pages:
// src/routes/+layout.server.ts (root)
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user,
appVersion: '2.1.0'
}
} // src/routes/dashboard/+layout.server.ts
export const load: LayoutServerLoad = async ({ locals, parent }) => {
// Get parent data
const parentData = await parent()
// Load dashboard-specific data
const stats = await getDashboardStats(locals.user.id)
return {
// Parent data is automatically included
stats
}
} <!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
import type { PageProps } from './$types'
let { data }: PageProps = $props()
// Has access to:
// - data.user (from root layout)
// - data.appVersion (from root layout)
// - data.stats (from dashboard layout)
</script> Security Best Practices
Don’t Expose Sensitive Data
// AVOID: Exposing all user data
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user // Might include passwordHash!
}
}
// PREFERRED: Only expose what's needed
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) return { user: null }
return {
user: {
id: locals.user.id,
name: locals.user.name,
email: locals.user.email,
avatar: locals.user.avatar,
role: locals.user.role
}
}
} Validate on Every Request
export const load: LayoutServerLoad = async ({ locals, cookies }) => {
// Even if locals.user exists, verify the session is still valid
const sessionId = cookies.get('session')
if (!sessionId) {
return { user: null }
}
// Verify session hasn't been revoked
const session = await db.sessions.findUnique({
where: { id: sessionId }
})
if (!session || session.revokedAt) {
cookies.delete('session', { path: '/' })
return { user: null }
}
return { user: locals.user }
} Conclusion
The +layout.server.js file represents the server-side counterpart to universal layout data loading, providing a secure foundation for authentication, authorization, and sensitive data access. By running exclusively on the server, it enables direct database access, private API keys, and session management without exposing implementation details or security credentials to the browser. Most commonly used in root layouts to establish user authentication context, it transforms security from a per-page concern into application-wide infrastructure.
Mastering +layout.server.js means understanding its relationship with +layout.js for hybrid data strategies, recognizing when data should be server-only versus universal, and implementing proper data sanitization before sending to clients.
Combined with layout groups for protected route sections and the locals object for request-scoped data, it provides the foundation for building secure, production-ready applications where authentication flows naturally from the root through your entire route hierarchy.
Key Takeaways
+layout.server.jsprovides server-only layout data that never runs in the browser, ideal for authentication, database queries, and sensitive operations using private credentials- Most commonly used in root layout (
src/routes/+layout.server.js) to establish app-wide user authentication context that flows to all pages automatically - Access server-only features including
cookiesfor session management,localsfor request-scoped data, andplatformfor deployment environment details - Data flows downstream to all descendants - child layouts and pages receive server layout data through the
dataprop without explicit passing - Combine with layout groups for protected sections - use
(authenticated)groups with+layout.server.jsto enforce auth requirements for entire route subtrees - Always sanitize data before client exposure - filter sensitive fields like password hashes, internal IDs, or admin flags before returning data
- Can coexist with
+layout.jsfor hybrid strategies - server loads auth/sensitive data, universal loads public data like navigation or categories - Returned data must be serializable - no functions, classes, or circular references, only JSON-compatible data structures
See Also
- Official SvelteKit Documentation - +layout.server.js
- Cookies - Session management and authentication
- Load Function Context - Understanding
locals,cookies,platform - Layout Groups - Organizing protected routes
- Authentication Patterns - Building secure auth flows