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 Data

The root +layout.server.js runs 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.js provides 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 cookies for session management, locals for request-scoped data, and platform for deployment environment details
  • Data flows downstream to all descendants - child layouts and pages receive server layout data through the data prop without explicit passing
  • Combine with layout groups for protected sections - use (authenticated) groups with +layout.server.js to 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.js for 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