Setting up Server-Side Auth for SvelteKit
Set up Server-Side Auth to use cookie-based authentication with SvelteKit.
Install Supabase packages Install the @supabase/supabase-js
package and the helper @supabase/ssr
package.
_10 npm install @supabase/supabase-js @supabase/ssr
Set up environment variables Create a .env.local
file in your project root directory.
Fill in your PUBLIC_SUPABASE_URL
and PUBLIC_SUPABASE_ANON_KEY
:
_10 PUBLIC_SUPABASE_URL=<your_supabase_project_url>
_10 PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
Set up server-side hooks 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.
_81 import { createServerClient } from '@supabase/ssr'
_81 import { type Handle, redirect } from '@sveltejs/kit'
_81 import { sequence } from '@sveltejs/kit/hooks'
_81 import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
_81 const supabase: Handle = async ({ event, resolve }) => {
_81 * Creates a Supabase client specific to this server request.
_81 * The Supabase client gets the Auth token from the request cookies.
_81 event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_81 getAll: () => event.cookies.getAll(),
_81 * SvelteKit's cookies API requires `path` to be explicitly set in
_81 * the cookie options. Setting `path` to `/` replicates previous/
_81 setAll: (cookiesToSet) => {
_81 cookiesToSet.forEach(({ name, value, options }) => {
_81 event.cookies.set(name, value, { ...options, path: '/' })
_81 * Unlike `supabase.auth.getSession()`, which returns the session _without_
_81 * validating the JWT, this function also calls `getUser()` to validate the
_81 * JWT before returning the session.
_81 event.locals.safeGetSession = async () => {
_81 } = await event.locals.supabase.auth.getSession()
_81 return { session: null, user: null }
_81 } = await event.locals.supabase.auth.getUser()
_81 // JWT validation has failed
_81 return { session: null, user: null }
_81 return { session, user }
_81 return resolve(event, {
_81 filterSerializedResponseHeaders(name) {
_81 * Supabase libraries use the `content-range` and `x-supabase-api-version`
_81 * headers, so we need to tell SvelteKit to pass it through.
_81 return name === 'content-range' || name === 'x-supabase-api-version'
_81 const authGuard: Handle = async ({ event, resolve }) => {
_81 const { session, user } = await event.locals.safeGetSession()
_81 event.locals.session = session
_81 event.locals.user = user
_81 if (!event.locals.session && event.url.pathname.startsWith('/private')) {
_81 redirect(303, '/auth')
_81 if (event.locals.session && event.url.pathname === '/auth') {
_81 redirect(303, '/private')
_81 return resolve(event)
_81 export const handle: Handle = sequence(supabase, authGuard)
Create TypeScript definitions To prevent TypeScript errors, add type definitions for the new event.locals
properties.
_20 import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
_20 // interface Error {}
_20 supabase: SupabaseClient
_20 safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
_20 session: Session | null
_20 session: Session | null
_20 // interface PageState {}
_20 // interface Platform {}
Create a Supabase client in your root layout 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
.
src/routes/ +layout.server.ts
_43 import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
_43 import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
_43 import type { LayoutLoad } from './$types'
_43 export const load: LayoutLoad = async ({ data, depends, fetch }) => {
_43 * Declare a dependency so the layout can be invalidated, for example, on
_43 depends('supabase:auth')
_43 const supabase = isBrowser()
_43 ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_43 : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_43 * It's fine to use `getSession` here, because on the client, `getSession` is
_43 * safe, and on the server, it reads `session` from the `LayoutData`, which
_43 * safely checked the session using `safeGetSession`.
_43 } = await supabase.auth.getSession()
_43 } = await supabase.auth.getUser()
_43 return { session, supabase, user }
Listen to Auth events Set up a listener for Auth events on the client, to handle session refreshes and signouts.
src/routes/ +layout.svelte
_19 import { invalidate } from '$app/navigation'
_19 import { onMount } from 'svelte'
_19 let { data, children } = $props()
_19 let { session, supabase } = $derived(data)
_19 const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
_19 if (newSession?.expires_at !== session?.expires_at) {
_19 invalidate('supabase:auth')
_19 return () => data.subscription.unsubscribe()
Create your first page 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 from your dashboard.
src/routes/ +page.server.ts
_10 import type { PageServerLoad } from './$types'
_10 export const load: PageServerLoad = async ({ locals: { supabase } }) => {
_10 const { data: countries } = await supabase.from('countries').select('name').limit(5).order('name')
_10 return { countries: countries ?? [] }
Change the Auth confirmation path 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 page in your dashboard. In the Confirm signup
template, change {{ .ConfirmationURL }}
to {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email
.
Create a login page Next, create a login page to let users sign up and log in.
src/routes/auth/ +page.server.ts
src/routes/auth/ +page.svelte
src/routes/auth/ +layout.svelte
src/routes/auth/error/ +page.svelte
_32 import { redirect } from '@sveltejs/kit'
_32 import type { Actions } from './$types'
_32 export const actions: Actions = {
_32 signup: async ({ request, locals: { supabase } }) => {
_32 const formData = await request.formData()
_32 const email = formData.get('email') as string
_32 const password = formData.get('password') as string
_32 const { error } = await supabase.auth.signUp({ email, password })
_32 redirect(303, '/auth/error')
_32 login: async ({ request, locals: { supabase } }) => {
_32 const formData = await request.formData()
_32 const email = formData.get('email') as string
_32 const password = formData.get('password') as string
_32 const { error } = await supabase.auth.signInWithPassword({ email, password })
_32 redirect(303, '/auth/error')
_32 redirect(303, '/private')
Create the signup confirmation route Finish the signup flow by creating the API route to handle email verification.
src/routes/auth/confirm/ +server.ts
_31 import type { EmailOtpType } from '@supabase/supabase-js'
_31 import { redirect } from '@sveltejs/kit'
_31 import type { RequestHandler } from './$types'
_31 export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
_31 const token_hash = url.searchParams.get('token_hash')
_31 const type = url.searchParams.get('type') as EmailOtpType | null
_31 const next = url.searchParams.get('next') ?? '/'
_31 * Clean up the redirect URL by deleting the Auth flow parameters.
_31 * `next` is preserved for now, because it's needed in the error case.
_31 const redirectTo = new URL(url)
_31 redirectTo.pathname = next
_31 redirectTo.searchParams.delete('token_hash')
_31 redirectTo.searchParams.delete('type')
_31 if (token_hash && type) {
_31 const { error } = await supabase.auth.verifyOtp({ type, token_hash })
_31 redirectTo.searchParams.delete('next')
_31 redirect(303, redirectTo)
_31 redirectTo.pathname = '/auth/error'
_31 redirect(303, redirectTo)
Create private routes 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
.
src/routes/private/ +layout.server.ts
src/routes/private/ +layout.svelte
src/routes/private/ +page.server.ts
src/routes/private/ +page.svelte
_10 * This file is necessary to ensure protection of all routes in the `private`
_10 * directory. It makes the routes in this directory _dynamic_ routes, which
_10 * send a server request, and thus trigger `hooks.server.ts`.