Svelte’s reactive model and server-side load functions make auth particularly clean. Here’s a minimal pattern that handles sign-in, sign-out, session persistence, and route protection in under 20 lines of application code.
The pieces
1. An auth store
// src/lib/auth.ts
import { writable } from 'svelte/store'
export type User = { id: string; email: string; name?: string } | null
export const user = writable<User>(null)
2. A server-side session hydration
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ cookies }) => {
const sessionId = cookies.get('session')
if (!sessionId) return { user: null }
const user = await getUserFromSession(sessionId)
return { user }
}
3. A client-side rehydration
<!-- src/routes/+layout.svelte -->
<script>
import { onMount } from 'svelte'
import { user } from '$lib/auth'
export let data
onMount(() => user.set(data.user))
</script>
<slot />
4. A redirect guard for protected routes
// src/routes/app/+layout.server.ts
import { redirect } from '@sveltejs/kit'
export const load = ({ parent }) => parent().then(({ user }) => {
if (!user) throw redirect(302, '/login')
return { user }
})
That’s it
Total line count: 18, not counting the getUserFromSession implementation (which is just a DB lookup).
The model:
- SSR-first: the session is resolved server-side on every navigation, so the user is always up-to-date on page loads
- Client store: the user is reactive in every component via
$user - Protected routes: a single
+layout.server.tsin any subtree enforces auth for everything under it
Sign-in flow
<script>
async function signIn() {
const res = await fetch('/api/auth/sign-in', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
if (res.ok) {
const { user: u } = await res.json()
user.set(u)
goto('/app')
}
}
</script>
The API route (src/routes/api/auth/sign-in/+server.ts) validates the credentials, creates a session, sets the cookie, returns the user. On client, we update the store and navigate.
Sign-out flow
<script>
import { user } from '$lib/auth'
import { goto } from '$app/navigation'
async function signOut() {
await fetch('/api/auth/sign-out', { method: 'POST' })
user.set(null)
goto('/')
}
</script>
Why Svelte makes this shorter
- Stores are primitive, don’t need a provider wrapper like React Context
- Server-side load functions run on every navigation, so session hydration is automatic
- Redirect from load is built into SvelteKit — no custom route-guard component needed
- Cookies are first-class in load functions via the
cookiesparameter
None of this is unique to Svelte, but the ergonomics make it the shortest implementation you can write without sacrificing correctness.