This episode covers the security of user identity: auth patterns in SvelteKit, session management and secure cookies, protected routes and route guards, and role-based access control and client-side and server-side auth.

An app that holds user data must know who is using it. That identity determines what can be seen and changed. Without a proper auth layer, endpoints that should be private can be read by anyone.
Auth is two different things. Authentication verifies identity — who you are — while authorization determines access rights — what you're allowed to do. SvelteKit doesn't ship a built-in auth solution, but it provides all the primitives to build one: cookies, hooks, and load functions.
This episode covers auth patterns in SvelteKit, session management and secure cookies, protected routes and route guards, and role-based access control and client/server auth. When you're done, you can build a secure login system for a production app.
Unlike monolithic frameworks, SvelteKit leaves the choice of auth mechanism to you. The two most common patterns are session-based auth and token-based auth. The first stores a session on the server and sends a cookie containing a token, while the second carries a signed token in the Authorization header.
Session-based auth is simple, easy to revoke, and suitable for most apps. Token-based auth fits public APIs better, with the trade-off that tokens are hard to invalidate before they expire.
You can write it from scratch. Hash passwords using bcryptjs:
npm install bcryptjsThe most common flow in SvelteKit starts with a login form in a component, continues with a form action on the server, then a session cookie. The form action runs entirely on the server, so the password never reaches client JavaScript. This is the main reason SvelteKit recommends form actions for authentication.
Cookies are the most practical session storage, as long as their security attributes are correct:
import { redirect } from "@sveltejs/kit"
export const actions = {
async login({ request, cookies }) {
const data = await request.formData()
const email = data.get("email")
const token = await buatToken(email)
cookies.set("session", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7,
})
throw redirect(303, "/dashboard")
},
}httpOnly: true makes the cookie unreadable by JavaScript, fending off XSS attacks that try to steal the session. secure: true forces the cookie to be sent over HTTPS only, and sameSite: "lax" protects against CSRF attacks. This combination is the minimum standard for auth cookies.
handle in hooks.server.js runs before every request. This is where the token is turned into a user object accessible from every load function and form action:
import { verifikasiSession } from "$lib/server/session"
export async function handle({ event, resolve }) {
const token = event.cookies.get("session")
event.locals.user = token
? await verifikasiSession(token)
: null
return resolve(event)
}event.locals.user is null when not logged in and a user object when the session is valid. With this pattern, the whole app reads identity from one source, without every page re-validating the token repeatedly.
Pages that require login must not just hide in the UI. The guard must run on the server before the page renders:
import { redirect } from "@sveltejs/kit"
export async function load({ locals }) {
if (!locals.user) {
throw redirect(303, "/masuk")
}
return { user: locals.user }
}Throwing redirect in a load function is SvelteKit's idiomatic way to redirect users who aren't logged in. Because +page.server.js runs on the server, users can't bypass it through DevTools.
When many pages need the same protection, put the guard in the parent's +layout.server.js. Once placed in a layout, all child routes are automatically protected. This pattern avoids code duplication between pages and makes access-policy changes easy.
Once the user is authenticated, authorization determines access rights based on role:
import { error } from "@sveltejs/kit"
export function requireRole(user, role) {
if (!user || !user.roles.includes(role)) {
throw error(403, "Akses ditolak")
}
}user.roles.includes(role) checks whether the required role is in the user's list of roles. When it isn't, the function throws 403 Forbidden. This check should always happen on the server — a client can be tricked by tampering with the response.
On the client, auth serves the experience: hiding admin buttons, showing the user's name, or redirecting to the login page:
<script>
import { onMount } from "svelte"
import { goto } from "$app/navigation"
let { data } = $props()
onMount(() => {
if (!data.user) {
goto("/masuk")
}
})
</script>This client guard is for experience only, not security. Since it runs after the page loads, sensitive content must still be protected on the server. goto("/masuk") redirects the user without reloading the page.
Key takeaways:
httpOnly, secure, and sameSite.hooks.server.js and inject it into event.locals.+page.server.js or +layout.server.js with redirects.Next, in episode 13 we will discuss secure data fetching — secure API consumption and token handling, CSRF, XSS, and input sanitization, and best practices for sensitive data. The session from this episode becomes the key that protects every API request.