--- title: 'Setting up Server-Side Auth for SvelteKit' hideToc: true --- Set up Server-Side Auth to use cookie-based authentication with SvelteKit. Install the `@supabase/supabase-js` package and the helper `@supabase/ssr` package. ```sh npm install @supabase/supabase-js @supabase/ssr ``` Create a `.env.local` file in your project root directory. Fill in your `PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_ANON_KEY`: ```txt .env.local PUBLIC_SUPABASE_URL= PUBLIC_SUPABASE_ANON_KEY= ``` Set up server-side hooks in `src/hooks.server.ts`. The hooks: - Create a request-specific Supabase client, using the user credentials from the request cookie. This client is used for server-only code. - Check user authentication. - Guard protected pages. ```ts src/hooks.server.ts import { createServerClient } from '@supabase/ssr' import { type Handle, redirect } from '@sveltejs/kit' import { sequence } from '@sveltejs/kit/hooks' import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' const supabase: Handle = async ({ event, resolve }) => { /** * Creates a Supabase client specific to this server request. * * The Supabase client gets the Auth token from the request cookies. */ event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), /** * SvelteKit's cookies API requires `path` to be explicitly set in * the cookie options. Setting `path` to `/` replicates previous/ * standard behavior. */ setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => { event.cookies.set(name, value, { ...options, path: '/' }) }) }, }, }) /** * Unlike `supabase.auth.getSession()`, which returns the session _without_ * validating the JWT, this function also calls `getUser()` to validate the * JWT before returning the session. */ event.locals.safeGetSession = async () => { const { data: { session }, } = await event.locals.supabase.auth.getSession() if (!session) { return { session: null, user: null } } const { data: { user }, error, } = await event.locals.supabase.auth.getUser() if (error) { // JWT validation has failed return { session: null, user: null } } return { session, user } } return resolve(event, { filterSerializedResponseHeaders(name) { /** * Supabase libraries use the `content-range` and `x-supabase-api-version` * headers, so we need to tell SvelteKit to pass it through. */ return name === 'content-range' || name === 'x-supabase-api-version' }, }) } const authGuard: Handle = async ({ event, resolve }) => { const { session, user } = await event.locals.safeGetSession() event.locals.session = session event.locals.user = user if (!event.locals.session && event.url.pathname.startsWith('/private')) { redirect(303, '/auth') } if (event.locals.session && event.url.pathname === '/auth') { redirect(303, '/private') } return resolve(event) } export const handle: Handle = sequence(supabase, authGuard) ``` To prevent TypeScript errors, add type definitions for the new `event.locals` properties. ```ts src/app.d.ts import type { Session, SupabaseClient, User } from '@supabase/supabase-js' declare global { namespace App { // interface Error {} interface Locals { supabase: SupabaseClient safeGetSession: () => Promise<{ session: Session | null; user: User | null }> session: Session | null user: User | null } interface PageData { session: Session | null } // interface PageState {} // interface Platform {} } } export {} ``` Create a Supabase client in your root `+layout.ts`. This client can be used to access Supabase from the client or the server. In order to get access to the Auth token on the server, use a `+layout.server.ts` file to pass in the session from `event.locals`. ```ts src/routes/+layout.ts import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' import type { LayoutLoad } from './$types' export const load: LayoutLoad = async ({ data, depends, fetch }) => { /** * Declare a dependency so the layout can be invalidated, for example, on * session refresh. */ depends('supabase:auth') const supabase = isBrowser() ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { fetch, }, }) : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { fetch, }, cookies: { getAll() { return data.cookies }, }, }) /** * It's fine to use `getSession` here, because on the client, `getSession` is * safe, and on the server, it reads `session` from the `LayoutData`, which * safely checked the session using `safeGetSession`. */ const { data: { session }, } = await supabase.auth.getSession() const { data: { user }, } = await supabase.auth.getUser() return { session, supabase, user } } ``` ```ts src/routes/+layout.server.ts import type { LayoutServerLoad } from './$types' export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => { const { session } = await safeGetSession() return { session, cookies: cookies.getAll(), } } ``` Set up a listener for Auth events on the client, to handle session refreshes and signouts. ```svelte src/routes/+layout.svelte ``` Create your first page. This example page calls Supabase from the server to get a list of countries from the database. This is an example of a public page that uses publicly readable data. To populate your database, run the [countries quickstart](/dashboard/project/_/sql/quickstarts) from your dashboard. ```ts src/routes/+page.server.ts import type { PageServerLoad } from './$types' export const load: PageServerLoad = async ({ locals: { supabase } }) => { const { data: countries } = await supabase.from('countries').select('name').limit(5).order('name') return { countries: countries ?? [] } } ``` ```svelte src/routes/+page.svelte

Welcome to Supabase!

    {#each countries as country}
  • {country.name}
  • {/each}
```
If you have email confirmation turned on (the default), a new user will receive an email confirmation after signing up. Change the email template to support a server-side authentication flow. Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`. Next, create a login page to let users sign up and log in. ```ts src/routes/auth/+page.server.ts import { redirect } from '@sveltejs/kit' import type { Actions } from './$types' export const actions: Actions = { signup: async ({ request, locals: { supabase } }) => { const formData = await request.formData() const email = formData.get('email') as string const password = formData.get('password') as string const { error } = await supabase.auth.signUp({ email, password }) if (error) { console.error(error) redirect(303, '/auth/error') } else { redirect(303, '/') } }, login: async ({ request, locals: { supabase } }) => { const formData = await request.formData() const email = formData.get('email') as string const password = formData.get('password') as string const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { console.error(error) redirect(303, '/auth/error') } else { redirect(303, '/private') } }, } ``` ```svelte src/routes/auth/+page.svelte
``` ```svelte src/routes/auth/+layout.svelte
``` ```svelte src/routes/auth/error/+page.svelte

Login error

```
Finish the signup flow by creating the API route to handle email verification. ```ts src/routes/auth/confirm/+server.ts import type { EmailOtpType } from '@supabase/supabase-js' import { redirect } from '@sveltejs/kit' import type { RequestHandler } from './$types' export const GET: RequestHandler = async ({ url, locals: { supabase } }) => { const token_hash = url.searchParams.get('token_hash') const type = url.searchParams.get('type') as EmailOtpType | null const next = url.searchParams.get('next') ?? '/' /** * Clean up the redirect URL by deleting the Auth flow parameters. * * `next` is preserved for now, because it's needed in the error case. */ const redirectTo = new URL(url) redirectTo.pathname = next redirectTo.searchParams.delete('token_hash') redirectTo.searchParams.delete('type') if (token_hash && type) { const { error } = await supabase.auth.verifyOtp({ type, token_hash }) if (!error) { redirectTo.searchParams.delete('next') redirect(303, redirectTo) } } redirectTo.pathname = '/auth/error' redirect(303, redirectTo) } ``` Create private routes that can only be accessed by authenticated users. The routes in the `private` directory are protected by the route guard in `hooks.server.ts`. To ensure that `hooks.server.ts` runs for every nested path, put a `+layout.server.ts` file in the `private` directory. This file can be empty, but must exist to protect routes that don't have their own `+layout|page.server.ts`. ```ts src/routes/private/+layout.server.ts /** * This file is necessary to ensure protection of all routes in the `private` * directory. It makes the routes in this directory _dynamic_ routes, which * send a server request, and thus trigger `hooks.server.ts`. **/ ``` ```svelte src/routes/private/+layout.svelte
``` ```sql SQL -- Run this SQL against your database to create a `notes` table. create table notes ( id bigint primary key generated always as identity, created_at timestamp with time zone not null default now(), user_id uuid references auth.users on delete cascade not null default auth.uid(), note text not null ); alter table notes enable row level security; revoke all on table notes from authenticated; revoke all on table notes from anon; grant all (note) on table notes to authenticated; grant select (id) on table notes to authenticated; grant delete on table notes to authenticated; create policy "Users can access and modify their own notes" on notes for all to authenticated using ((select auth.uid()) = user_id); ``` ```svelte src/routes/private/+page.server.ts import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ depends, locals: { supabase } }) => { depends('supabase:db:notes'); const { data: notes } = await supabase.from('notes').select('id,note').order('id'); return { notes: notes ?? [] }; }; ``` ```svelte src/routes/private/+page.svelte

Private page for user: {user?.email}

Notes

    {#each notes as note}
  • {note.note}
  • {/each}
```