Session Management
Sessions connect a browser to a server-side identity. When a user logs in, you create a session, store a reference to it in a cookie, and use that cookie on subsequent requests to identify who is making the request. SvelteKit provides a clean cookies API through event.cookies that handles the complexity of secure cookie management.
The Cookies API
SvelteKit's event.cookies provides typed methods for managing cookies:
// src/routes/api/example/+server.ts
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies }) => {
// Read a cookie
const session = cookies.get('session');
// Set a cookie with options
cookies.set('session', 'abc123', {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 days
});
// Delete a cookie
cookies.delete('session', { path: '/' });
return new Response('ok');
};
Unlike raw Set-Cookie headers, event.cookies handles serialization, parsing, and ensures you set cookies correctly. The path option is required when setting or deleting cookies because SvelteKit does not assume a default.
Cookie Security Options
Every session cookie should use these security options:
// src/lib/server/session.ts
import type { Cookies } from '@sveltejs/kit';
const SESSION_COOKIE = 'session';
export function setSessionCookie(cookies: Cookies, token: string) {
cookies.set(SESSION_COOKIE, token, {
path: '/', // Cookie sent for all routes
httpOnly: true, // JavaScript cannot read it (prevents XSS theft)
secure: true, // Only sent over HTTPS
sameSite: 'lax', // Sent with top-level navigations, not cross-site requests
maxAge: 60 * 60 * 24 * 7 // Expires in 7 days
});
}
export function clearSessionCookie(cookies: Cookies) {
cookies.delete(SESSION_COOKIE, { path: '/' });
}
export function getSessionToken(cookies: Cookies): string | undefined {
return cookies.get(SESSION_COOKIE);
}
HttpOnly prevents client-side JavaScript from reading the cookie. This stops XSS attacks from stealing sessions. Secure ensures the cookie is only sent over HTTPS. SameSite: lax allows the cookie on same-site requests and top-level navigations (clicking a link) but blocks it on cross-site POST requests, which mitigates CSRF attacks.
Server-Side Sessions
Server-side sessions store the session data on the server and only put an opaque token in the cookie. This is the more secure approach because the client never sees the actual session data:
// src/lib/server/session.ts
import { randomBytes } from 'crypto';
interface SessionData {
userId: string;
createdAt: number;
expiresAt: number;
}
// In production, use Redis, a database, or Cloudflare KV
const sessions = new Map<string, SessionData>();
export function createSession(userId: string): string {
const token = randomBytes(32).toString('hex');
const now = Date.now();
sessions.set(token, {
userId,
createdAt: now,
expiresAt: now + 7 * 24 * 60 * 60 * 1000 // 7 days
});
return token;
}
export function getSession(token: string): SessionData | null {
const session = sessions.get(token);
if (!session) return null;
if (Date.now() > session.expiresAt) {
sessions.delete(token);
return null;
}
return session;
}
export function destroySession(token: string): void {
sessions.delete(token);
}
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { getSession } from '$lib/server/session';
import { getUserById } from '$lib/server/db';
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session');
if (token) {
const session = getSession(token);
if (session) {
event.locals.user = await getUserById(session.userId);
} else {
event.cookies.delete('session', { path: '/' });
event.locals.user = null;
}
} else {
event.locals.user = null;
}
return resolve(event);
};
JWT Sessions
JWTs encode the session data directly in the token. No server-side storage is needed. The trade-off is that you cannot revoke individual sessions without maintaining a blocklist:
// src/lib/server/jwt.ts
import { SignJWT, jwtVerify } from 'jose';
import { JWT_SECRET } from '$env/static/private';
const secret = new TextEncoder().encode(JWT_SECRET);
export async function createToken(userId: string, role: string): Promise<string> {
return new SignJWT({ userId, role })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(secret);
}
export async function verifyToken(token: string): Promise<{
userId: string;
role: string;
} | null> {
try {
const { payload } = await jwtVerify(token, secret);
return {
userId: payload.userId as string,
role: payload.role as string
};
} catch {
return null;
}
}
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { verifyToken } from '$lib/server/jwt';
import { getUserById } from '$lib/server/db';
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session');
if (token) {
const payload = await verifyToken(token);
if (payload) {
event.locals.user = await getUserById(payload.userId);
} else {
event.cookies.delete('session', { path: '/' });
event.locals.user = null;
}
} else {
event.locals.user = null;
}
return resolve(event);
};
The Login Flow
A complete login implementation using form actions:
// src/routes/login/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { verifyPassword } from '$lib/server/auth';
import { createSession } from '$lib/server/session';
import { setSessionCookie } from '$lib/server/session';
export const load: PageServerLoad = async ({ locals }) => {
// Already logged in — redirect to app
if (locals.user) {
redirect(303, '/app');
}
};
export const actions: Actions = {
default: async ({ request, cookies, url }) => {
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
if (!email || !password) {
return fail(400, {
email,
error: 'Email and password are required'
});
}
const user = await verifyPassword(email, password);
if (!user) {
return fail(400, {
email,
error: 'Invalid email or password'
});
}
// Create session and set cookie
const token = createSession(user.id);
setSessionCookie(cookies, token);
// Redirect to the return URL or default to /app
const returnTo = url.searchParams.get('returnTo') || '/app';
redirect(303, returnTo);
}
};
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
import { enhance } from '$app/forms';
let { form }: { form: ActionData } = $props();
</script>
<h1>Log In</h1>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST" use:enhance>
<label>
Email
<input type="email" name="email" value={form?.email ?? ''} required />
</label>
<label>
Password
<input type="password" name="password" required />
</label>
<button type="submit">Log In</button>
</form>
The Logout Flow
Logout destroys the session and clears the cookie:
// src/routes/logout/+page.server.ts
import type { Actions } from './$types';
import { redirect } from '@sveltejs/kit';
import { destroySession, clearSessionCookie, getSessionToken } from '$lib/server/session';
export const actions: Actions = {
default: async ({ cookies }) => {
const token = getSessionToken(cookies);
if (token) {
destroySession(token);
}
clearSessionCookie(cookies);
redirect(303, '/');
}
};
<!-- Logout button used in navigation -->
<form method="POST" action="/logout">
<button type="submit">Log Out</button>
</form>
Using a POST action for logout prevents CSRF issues. A GET request to /logout could be triggered by an image tag or link prefetch.
Integrating with Auth Providers
Modern applications often delegate authentication to a dedicated provider. The pattern is similar regardless of the provider: redirect to the provider, receive a callback with tokens, create a local session.
Lucia (self-hosted auth library):
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { lucia } from '$lib/server/lucia';
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(lucia.sessionCookieName);
if (sessionId) {
const { session, user } = await lucia.validateSession(sessionId);
if (session?.fresh) {
const cookie = lucia.createSessionCookie(session.id);
event.cookies.set(cookie.name, cookie.value, {
path: '/',
...cookie.attributes
});
}
if (!session) {
const cookie = lucia.createBlankSessionCookie();
event.cookies.set(cookie.name, cookie.value, {
path: '/',
...cookie.attributes
});
}
event.locals.user = user;
event.locals.session = session;
} else {
event.locals.user = null;
event.locals.session = null;
}
return resolve(event);
};
Supabase Auth:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { createServerClient } from '@supabase/ssr';
import { PUBLIC_SUPABASE_URL } from '$env/static/public';
import { SUPABASE_SERVICE_KEY } from '$env/static/private';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createServerClient(
PUBLIC_SUPABASE_URL,
SUPABASE_SERVICE_KEY,
{
cookies: {
getAll: () => event.cookies.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: '/' });
});
}
}
}
);
const { data: { user } } = await event.locals.supabase.auth.getUser();
event.locals.user = user;
return resolve(event);
};
Reading Sessions in Load Functions
The session data flows from hooks to load functions through locals:
// src/routes/app/+layout.server.ts
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
redirect(303, '/login');
}
return {
user: {
id: locals.user.id,
name: locals.user.name,
email: locals.user.email
}
};
};
Common Pitfalls
- Storing sensitive data in cookies directly. Never put user roles, permissions, or personal data in a cookie value. Use an opaque session token and store the data server-side.
- Forgetting the
pathoption. SvelteKit requires you to specifypathwhen setting or deleting cookies. Without it, the operation fails silently or applies to the wrong path. - Not setting HttpOnly. Without
httpOnly: true, client-side JavaScript can read the session cookie. An XSS vulnerability becomes a session hijacking vulnerability. - Using GET for logout. A GET request to
/logoutcan be triggered by link prefetching, image tags, or CSRF attacks. Always use a POST form action for logout. - Not refreshing expiring sessions. If a user is active but their session expires, they are logged out mid-task. Extend the session on activity by resetting
maxAgeor creating a new session periodically. - In-memory session storage in serverless. The
Mapexamples above work for adapter-node but not for serverless functions that may not share memory across invocations. Use a database or external store.
Key Takeaways
- Use
event.cookies.get/set/deletefor all cookie operations. Always specify thepathoption. - Session cookies should be
httpOnly,secure, andsameSite: 'lax'at minimum. - Server-side sessions (opaque token in cookie, data in database) are more secure than JWTs because you can revoke them instantly.
- JWTs avoid server-side storage but cannot be revoked without a blocklist.
- The pattern is consistent: set session cookie in the login action, read it in the handle hook, clear it in the logout action.
- Auth providers like Lucia and Supabase Auth follow the same pattern but handle token management and user storage for you.
- Always use POST form actions for logout to prevent CSRF-triggered logouts.