React
Authentication hooks in SvelteKit: handle, locals and redirect
In SvelteKit authentication lives in hooks.server.ts: handle reads the cookie, fills locals, and the redirect sits in the protected group's load.
Key points
- hooks.server.ts is the only place that sees every request: it reads the cookie and fills event.locals.
- The locals type is declared in app.d.ts, otherwise TypeScript sees an empty object.
- The redirect for a protected page belongs in the group's +layout.server.ts, not in the component.
In SvelteKit authentication hangs off handle in src/hooks.server.ts: the function runs on every request, reads the session cookie and fills event.locals, which load functions and form actions read without repeating the query.
handle sees every request
The file is called hooks.server.ts and has no client-side equivalent: no browser code can write locals. The type is declared in app.d.ts, otherwise TypeScript sees an empty object.
openssl rand -base64 32
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get("session");
event.locals.user = token ? await readSession(token) : null;
return resolve(event);
};
The type declaration lives in src/app.d.ts and covers the whole project: without it, every read of locals.user is a compile error.
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user: { id: string; role: "admin" | "member" } | null;
}
}
}
export {};
The check sits in the group's load
The redirect does not belong in the component: it belongs in a +layout.server.ts that covers the protected group, so the rule lives in one file instead of ten.
// src/routes/(app)/+layout.server.ts
import { redirect } from "@sveltejs/kit";
export const load = ({ locals, url }) => {
if (!locals.user) redirect(303, `/login?next=${url.pathname}`);
return { user: locals.user };
};
Several hooks, in order
sequence() composes several handle functions: the first reads the cookie, the second logs the request, the third applies the rule. The order follows the arguments, so whatever reads locals.user comes after whatever writes it.
| Point | What it sees | What it is for |
|---|---|---|
| hooks.server.ts | Every request, API and files too | Read the cookie and fill locals |
| +layout.server.ts | Requests in the group | Block pages with a 303 redirect |
| +page.server.ts | One route | Check a permission on a single record |
| Form action | The POST | Verify the role before writing |
The limitation to plan for
Hooks do not run for prerendered pages: a page with export const prerender = true is built once and cannot check the session. And handle runs for assets too, so keep it short.
Mistakes you will see
- Reading the session in the component with
onMount: the first HTML has already gone out without a user. - Returning a response from
handlewithout callingresolve: the page is never rendered. - Putting the profile query in
handle: it runs for an image request too. - Storing the token in
localStorage: the server never sees it and the protected page stays reachable through a direct request.
The pages the hook protects are built from ready-made blocks: login and authentication cover sign-in and recovery, and flowbite-svelte supplies the fields. Data loading is in server vs universal load, validation in SvelteKit form actions, route files in routing and layouts in SvelteKit, the comparison in Svelte vs React.
Frequently asked questions
Can I protect only some pages?
Yes, by putting the check in a +layout.server.ts inside a route group, for example (app). Pages outside the group never pass through that load and stay public without touching the hook.
Where do I store the session?
In an httpOnly, secure, sameSite lax cookie holding an opaque identifier rather than user data. The cookie travels on every request, so keep it small and signed.
Why is locals empty in TypeScript?
Because the declaration in src/app.d.ts is missing. Without the App.Locals interface the compiler does not know the fields handle writes and reports them as absent.
Do I need a hook for authorisation?
No, reading the session in one hook is enough. Permission depends on the route and the record, so check it in the load or the action that touches that record, where the context is known.
How do several hooks work together?
With sequence(), which runs them in order and passes each resolve result to the next. Without sequence the last handle export wins and the others are ignored.