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.
_82 import { createServerClient } from '@supabase/ssr'
_82 import { type Handle, redirect } from '@sveltejs/kit'
_82 import { sequence } from '@sveltejs/kit/hooks'
_82 import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
_82 const supabase: Handle = async ({ event, resolve }) => {
_82 * Creates a Supabase client specific to this server request.
_82 * The Supabase client gets the Auth token from the request cookies.
_82 event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_82 get: (key) => event.cookies.get(key),
_82 * SvelteKit's cookies API requires `path` to be explicitly set in
_82 * the cookie options. Setting `path` to `/` replicates previous/
_82 set: (key, value, options) => {
_82 event.cookies.set(key, value, { ...options, path: '/' })
_82 remove: (key, options) => {
_82 event.cookies.delete(key, { ...options, path: '/' })
_82 * Unlike `supabase.auth.getSession()`, which returns the session _without_
_82 * validating the JWT, this function also calls `getUser()` to validate the
_82 * JWT before returning the session.
_82 event.locals.safeGetSession = async () => {
_82 } = await event.locals.supabase.auth.getSession()
_82 return { session: null, user: null }
_82 } = await event.locals.supabase.auth.getUser()
_82 // JWT validation has failed
_82 return { session: null, user: null }
_82 return { session, user }
_82 return resolve(event, {
_82 filterSerializedResponseHeaders(name) {
_82 * Supabase libraries use the `content-range` and `x-supabase-api-version`
_82 * headers, so we need to tell SvelteKit to pass it through.
_82 return name === 'content-range' || name === 'x-supabase-api-version'
_82 const authGuard: Handle = async ({ event, resolve }) => {
_82 const { session, user } = await event.locals.safeGetSession()
_82 event.locals.session = session
_82 event.locals.user = user
_82 if (!event.locals.session && event.url.pathname.startsWith('/private')) {
_82 return redirect(303, '/auth')
_82 if (event.locals.session && event.url.pathname === '/auth') {
_82 return redirect(303, '/private')
_82 return resolve(event)
_82 export const handle: Handle = sequence(supabase, authGuard)
Create TypeScript definitions To prevent TypeScript errors, add type definitions for the new event.locals
properties.
_18 import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
_18 // interface Error {}
_18 supabase: SupabaseClient
_18 safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
_18 session: Session | null
_18 // interface PageData {}
_18 // interface PageState {}
_18 // 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
_51 import { createBrowserClient, createServerClient, isBrowser, parse } from '@supabase/ssr'
_51 import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
_51 import type { LayoutLoad } from './$types'
_51 export const load: LayoutLoad = async ({ data, depends, fetch }) => {
_51 * Declare a dependency so the layout can be invalidated, for example, on
_51 depends('supabase:auth')
_51 const supabase = isBrowser()
_51 ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_51 const cookie = parse(document.cookie)
_51 : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_51 return JSON.stringify(data.session)
_51 * It's fine to use `getSession` here, because on the client, `getSession` is
_51 * safe, and on the server, it reads `session` from the `LayoutData`, which
_51 * safely checked the session using `safeGetSession`.
_51 } = await supabase.auth.getSession()
_51 } = await supabase.auth.getUser()
_51 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
_28 import { goto, invalidate } from '$app/navigation';
_28 import { onMount } from 'svelte';
_28 $: ({ session, supabase } = data);
_28 const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
_28 * Queue this as a task so the navigation won't prevent the
_28 * triggering function from completing
_28 goto('/', { invalidateAll: true });
_28 if (newSession?.expires_at !== session?.expires_at) {
_28 invalidate('supabase:auth');
_28 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 }}/api/auth/confirm?token_hash={{ .TokenHash }}&type=signup
.
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 return redirect(303, '/auth/error')
_32 return redirect(303, '/')
_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 return redirect(303, '/auth/error')
_32 return redirect(303, '/private')
Create the signup confirmation route Finish the signup flow by creating the API route to handle email verification.
src/routes/api/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 return redirect(303, redirectTo)
_31 redirectTo.pathname = '/auth/error'
_31 return 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`.