Auth

Setting up Server-Side Auth for SvelteKit


Set up Server-Side Auth to use cookie-based authentication with SvelteKit.

1

Install Supabase packages

Install the @supabase/supabase-js package and the helper @supabase/ssr package.


_10
npm install @supabase/supabase-js @supabase/ssr

2

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:

Project URL
Anon key
.env.local

_10
PUBLIC_SUPABASE_URL=<your_supabase_project_url>
_10
PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>

3

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.
src/hooks.server.ts

_82
import { createServerClient } from '@supabase/ssr'
_82
import { type Handle, redirect } from '@sveltejs/kit'
_82
import { sequence } from '@sveltejs/kit/hooks'
_82
_82
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
_82
_82
const supabase: Handle = async ({ event, resolve }) => {
_82
/**
_82
* Creates a Supabase client specific to this server request.
_82
*
_82
* The Supabase client gets the Auth token from the request cookies.
_82
*/
_82
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_82
cookies: {
_82
get: (key) => event.cookies.get(key),
_82
/**
_82
* SvelteKit's cookies API requires `path` to be explicitly set in
_82
* the cookie options. Setting `path` to `/` replicates previous/
_82
* standard behavior.
_82
*/
_82
set: (key, value, options) => {
_82
event.cookies.set(key, value, { ...options, path: '/' })
_82
},
_82
remove: (key, options) => {
_82
event.cookies.delete(key, { ...options, path: '/' })
_82
},
_82
},
_82
})
_82
_82
/**
_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
*/
_82
event.locals.safeGetSession = async () => {
_82
const {
_82
data: { session },
_82
} = await event.locals.supabase.auth.getSession()
_82
if (!session) {
_82
return { session: null, user: null }
_82
}
_82
_82
const {
_82
data: { user },
_82
error,
_82
} = await event.locals.supabase.auth.getUser()
_82
if (error) {
_82
// JWT validation has failed
_82
return { session: null, user: null }
_82
}
_82
_82
return { session, user }
_82
}
_82
_82
return resolve(event, {
_82
filterSerializedResponseHeaders(name) {
_82
/**
_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
*/
_82
return name === 'content-range' || name === 'x-supabase-api-version'
_82
},
_82
})
_82
}
_82
_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
_82
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
_82
return redirect(303, '/auth')
_82
}
_82
_82
if (event.locals.session && event.url.pathname === '/auth') {
_82
return redirect(303, '/private')
_82
}
_82
_82
return resolve(event)
_82
}
_82
_82
export const handle: Handle = sequence(supabase, authGuard)

4

Create TypeScript definitions

To prevent TypeScript errors, add type definitions for the new event.locals properties.

src/app.d.ts

_18
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
_18
_18
declare global {
_18
namespace App {
_18
// interface Error {}
_18
interface Locals {
_18
supabase: SupabaseClient
_18
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
_18
session: Session | null
_18
user: User | null
_18
}
_18
// interface PageData {}
_18
// interface PageState {}
_18
// interface Platform {}
_18
}
_18
}
_18
_18
export {}

5

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.ts
src/routes/+layout.server.ts

_51
import { createBrowserClient, createServerClient, isBrowser, parse } from '@supabase/ssr'
_51
_51
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
_51
_51
import type { LayoutLoad } from './$types'
_51
_51
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
_51
/**
_51
* Declare a dependency so the layout can be invalidated, for example, on
_51
* session refresh.
_51
*/
_51
depends('supabase:auth')
_51
_51
const supabase = isBrowser()
_51
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_51
global: {
_51
fetch,
_51
},
_51
cookies: {
_51
get(key) {
_51
const cookie = parse(document.cookie)
_51
return cookie[key]
_51
},
_51
},
_51
})
_51
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
_51
global: {
_51
fetch,
_51
},
_51
cookies: {
_51
get() {
_51
return JSON.stringify(data.session)
_51
},
_51
},
_51
})
_51
_51
/**
_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
*/
_51
const {
_51
data: { session },
_51
} = await supabase.auth.getSession()
_51
_51
const {
_51
data: { user },
_51
} = await supabase.auth.getUser()
_51
_51
return { session, supabase, user }
_51
}

6

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
<script>
_28
import { goto, invalidate } from '$app/navigation';
_28
import { onMount } from 'svelte';
_28
_28
export let data;
_28
$: ({ session, supabase } = data);
_28
_28
onMount(() => {
_28
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
_28
if (!newSession) {
_28
/**
_28
* Queue this as a task so the navigation won't prevent the
_28
* triggering function from completing
_28
*/
_28
setTimeout(() => {
_28
goto('/', { invalidateAll: true });
_28
});
_28
}
_28
if (newSession?.expires_at !== session?.expires_at) {
_28
invalidate('supabase:auth');
_28
}
_28
});
_28
_28
return () => data.subscription.unsubscribe();
_28
});
_28
</script>
_28
_28
<slot />

7

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
src/routes/+page.svelte

_10
import type { PageServerLoad } from './$types'
_10
_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 ?? [] }
_10
}

8

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.

9

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
_32
import type { Actions } from './$types'
_32
_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
_32
const { error } = await supabase.auth.signUp({ email, password })
_32
if (error) {
_32
console.error(error)
_32
return redirect(303, '/auth/error')
_32
} else {
_32
return redirect(303, '/')
_32
}
_32
},
_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
_32
const { error } = await supabase.auth.signInWithPassword({ email, password })
_32
if (error) {
_32
console.error(error)
_32
return redirect(303, '/auth/error')
_32
} else {
_32
return redirect(303, '/private')
_32
}
_32
},
_32
}

10

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
_31
import type { RequestHandler } from './$types'
_31
_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
_31
/**
_31
* Clean up the redirect URL by deleting the Auth flow parameters.
_31
*
_31
* `next` is preserved for now, because it's needed in the error case.
_31
*/
_31
const redirectTo = new URL(url)
_31
redirectTo.pathname = next
_31
redirectTo.searchParams.delete('token_hash')
_31
redirectTo.searchParams.delete('type')
_31
_31
if (token_hash && type) {
_31
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
_31
if (!error) {
_31
redirectTo.searchParams.delete('next')
_31
return redirect(303, redirectTo)
_31
}
_31
}
_31
_31
redirectTo.pathname = '/auth/error'
_31
return redirect(303, redirectTo)
_31
}

11

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
SQL
src/routes/private/+page.server.ts
src/routes/private/+page.svelte

_10
/**
_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`.
_10
**/