Supabase Auth with SvelteKit
We generally recommend using the new @supabase/ssr
package instead of auth-helpers
. @supabase/ssr
takes the core concepts of the Auth Helpers package and makes them available to any server framework. Check out the migration doc to learn more.
This submodule provides convenience helpers for implementing user authentication in SvelteKit applications.
This library supports Node.js ^16.15.0
.
_10 npm install @supabase/auth-helpers-sveltekit @supabase/supabase-js
Retrieve your project's URL and anon key from your API settings , and create a .env.local
file with the following environment variables:
_10 # Find these in your Supabase project settings https://supabase.com/dashboard/project/_/settings/api
_10 PUBLIC_SUPABASE_URL=https://your-project.supabase.co
_10 PUBLIC_SUPABASE_ANON_KEY=your-anon-key
JavaScript TypeScript
Create a new hooks.server.js
file in the root of your project and populate with the following:
_29 // src/hooks.server.js
_29 import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
_29 import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
_29 export const handle = async ({ event, resolve }) => {
_29 event.locals.supabase = createSupabaseServerClient({
_29 supabaseUrl: PUBLIC_SUPABASE_URL,
_29 supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
_29 * a little helper that is written for convenience so that instead
_29 * of calling `const { data: { session } } = await supabase.auth.getSession()`
_29 * you just call this `await getSession()`
_29 event.locals.getSession = async () => {
_29 } = await event.locals.supabase.auth.getSession()
_29 return resolve(event, {
_29 filterSerializedResponseHeaders(name) {
_29 return name === 'content-range'
Note that we are specifying filterSerializedResponseHeaders here. We need to tell SvelteKit that supabase needs the content-range header.
The Code Exchange
route is required for the server-side auth flow implemented by the SvelteKit Auth Helpers. It exchanges an auth code
for the user's session
, which is set as a cookie for future requests made to Supabase.
JavaScript TypeScript
Create a new file at src/routes/auth/callback/+server.js
and populate with the following:
src/routes/auth/callback/ +server.js
_11 import { redirect } from '@sveltejs/kit'
_11 export const GET = async ({ url, locals: { supabase } }) => {
_11 const code = url.searchParams.get('code')
_11 await supabase.auth.exchangeCodeForSession(code)
_11 throw redirect(303, '/')
In order to get the most out of TypeScript and it's intellisense, you should import the generated Database types into the app.d.ts
type definition file that comes with your SvelteKit project, where import('./DatabaseDefinitions')
points to the generated types file outlined in v2 docs here after you have logged in, linked, and generated types through the Supabase CLI.
_18 import { SupabaseClient, Session } from '@supabase/supabase-js'
_18 import { Database } from './DatabaseDefinitions'
_18 supabase: SupabaseClient<Database>
_18 getSession(): Promise<Session | null>
_18 session: Session | null
_18 // interface Error {}
_18 // interface Platform {}
Authentication can be initiated client or server-side . All of the supabase-js authentication strategies are supported with the Auth Helpers client.
Note: The authentication flow requires the Code Exchange Route to exchange a code
for the user's session
.
To make the session available across the UI, including pages and layouts, it is crucial to pass the session as a parameter in the root layout's server load function.
JavaScript TypeScript
src/routes/ +layout.server.js
_10 // src/routes/+layout.server.js
_10 export const load = async ({ locals: { getSession } }) => {
_10 session: await getSession(),
Shared load functions and pages#
To utilize Supabase in shared load functions and within pages, it is essential to create a Supabase client in the root layout load.
JavaScript TypeScript
_20 // src/routes/+layout.js
_20 import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
_20 import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit'
_20 export const load = async ({ fetch, data, depends }) => {
_20 depends('supabase:auth')
_20 const supabase = createSupabaseLoadClient({
_20 supabaseUrl: PUBLIC_SUPABASE_URL,
_20 supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
_20 serverSession: data.session,
_20 } = await supabase.auth.getSession()
_20 return { supabase, session }
Access the client inside pages by $page.data.supabase
or data.supabase
when using export let data
.
The usage of depends
tells sveltekit that this load function should be executed whenever invalidate
is called to keep the page store in sync.
createSupabaseLoadClient
caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs.
We need to create an event listener in the root +layout.svelte
file in order to catch supabase events being triggered.
src/routes/ +layout.svelte
_24 <!-- src/routes/+layout.svelte -->
_24 import { invalidate } from '$app/navigation'
_24 import { onMount } from 'svelte'
_24 let { supabase, session } = data
_24 $: ({ supabase, session } = data)
_24 data: { subscription },
_24 } = supabase.auth.onAuthStateChange((event, _session) => {
_24 if (_session?.expires_at !== session?.expires_at) {
_24 invalidate('supabase:auth')
_24 return () => subscription.unsubscribe()
The usage of invalidate
tells SvelteKit that the root +layout.ts
load function should be executed whenever the session updates to keep the page store in sync.
We can access the supabase instance in our +page.svelte
file through the data object.
src/routes/auth/ +page.svelte
_39 <!-- // src/routes/auth/+page.svelte -->
_39 let { supabase } = data
_39 $: ({ supabase } = data)
_39 const handleSignUp = async () => {
_39 await supabase.auth.signUp({
_39 emailRedirectTo: `${location.origin}/auth/callback`,
_39 const handleSignIn = async () => {
_39 await supabase.auth.signInWithPassword({
_39 const handleSignOut = async () => {
_39 await supabase.auth.signOut()
_39 <form on:submit="{handleSignUp}">
_39 <input name="email" bind:value="{email}" />
_39 <input type="password" name="password" bind:value="{password}" />
_39 <button>Sign up</button>
_39 <button on:click="{handleSignIn}">Sign in</button>
_39 <button on:click="{handleSignOut}">Sign out</button>
Form Actions can be used to trigger the authentication process from form submissions.
JavaScript TypeScript
src/routes/login/ +page.server.js
_27 // src/routes/login/+page.server.js
_27 import { fail } from '@sveltejs/kit'
_27 export const actions = {
_27 default: async ({ request, url, locals: { supabase } }) => {
_27 const formData = await request.formData()
_27 const email = formData.get('email')
_27 const password = formData.get('password')
_27 const { error } = await supabase.auth.signUp({
_27 emailRedirectTo: `${url.origin}/auth/callback`,
_27 return fail(500, { message: 'Server error. Try again later.', success: false, email })
_27 message: 'Please check your email for a magic link to log into the website.',
src/routes/login/ +page.svelte
_11 <!-- // src/routes/login/+page.svelte -->
_11 import { enhance } from '$app/forms'
_11 <form method="post" use:enhance>
_11 <input name="email" value={form?.email ?? ''} />
_11 <input type="password" name="password" />
_11 <button>Sign up</button>
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is null
.
src/routes/api/protected-route/ +server.ts
_13 // src/routes/api/protected-route/+server.ts
_13 import { json, error } from '@sveltejs/kit'
_13 export const GET = async ({ locals: { supabase, getSession } }) => {
_13 const session = await getSession()
_13 // the user is not signed in
_13 throw error(401, { message: 'Unauthorized' })
_13 const { data } = await supabase.from('test').select('*')
_13 return json({ data })
If you visit /api/protected-route
without a valid session cookie, you will get a 401 response.
Wrap an Action to check that the user has a valid session. If they're not logged in the session is null
.
src/routes/posts/ +page.server.ts
_29 // src/routes/posts/+page.server.ts
_29 import { error, fail } from '@sveltejs/kit'
_29 export const actions = {
_29 createPost: async ({ request, locals: { supabase, getSession } }) => {
_29 const session = await getSession()
_29 // the user is not signed in
_29 throw error(401, { message: 'Unauthorized' })
_29 // we are save, let the user create the post
_29 const formData = await request.formData()
_29 const content = formData.get('content')
_29 const { error: createPostError, data: newPost } = await supabase
_29 if (createPostError) {
_29 supabaseErrorMessage: createPostError.message,
If you try to submit a form with the action ?/createPost
without a valid session cookie, you will get a 401 error response.
To avoid writing the same auth logic in every single route you can also use the handle hook to
protect multiple routes at once. For this to work with your Supabase session, you need to use
Sveltekit's sequence helper function.
Edit your /src/hooks.server.js
with the below:
JavaScript TypeScript
_55 // src/hooks.server.js
_55 import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
_55 import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
_55 import { redirect, error } from '@sveltejs/kit'
_55 import { sequence } from '@sveltejs/kit/hooks'
_55 async function supabase({ event, resolve }) {
_55 event.locals.supabase = createSupabaseServerClient({
_55 supabaseUrl: PUBLIC_SUPABASE_URL,
_55 supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
_55 * a little helper that is written for convenience so that instead
_55 * of calling `const { data: { session } } = await supabase.auth.getSession()`
_55 * you just call this `await getSession()`
_55 event.locals.getSession = async () => {
_55 } = await event.locals.supabase.auth.getSession()
_55 return resolve(event, {
_55 filterSerializedResponseHeaders(name) {
_55 return name === 'content-range'
_55 async function authorization({ event, resolve }) {
_55 // protect requests to all routes that start with /protected-routes
_55 if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') {
_55 const session = await event.locals.getSession()
_55 // the user is not signed in
_55 throw redirect(303, '/')
_55 // protect POST requests to all routes that start with /protected-posts
_55 if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
_55 const session = await event.locals.getSession()
_55 // the user is not signed in
_55 throw error(303, '/')
_55 return resolve(event)
_55 export const handle = sequence(supabase, authorization)
For row level security to work properly when fetching data client-side, you need to use supabaseClient
from PageData
and only run your query once the session is defined client-side:
_18 async function loadData() {
_18 const { data: result } = await data.supabase.from('test').select('*').limit(20)
_18 $: if (data.session) {
_18 <p>client-side data fetching with RLS</p>
_18 <pre>{JSON.stringify(loadedData, null, 2)}</pre>
src/routes/profile/ +page.svelte
_11 <!-- src/routes/profile/+page.svelte -->
_11 let { user, tableData } = data
_11 $: ({ user, tableData } = data)
_11 <div>Protected content for {user.email}</div>
_11 <pre>{JSON.stringify(tableData, null, 2)}</pre>
_11 <pre>{JSON.stringify(user, null, 2)}</pre>
src/routes/profile/ +page.ts
_15 // src/routes/profile/+page.ts
_15 import { redirect } from '@sveltejs/kit'
_15 export const load = async ({ parent }) => {
_15 const { supabase, session } = await parent()
_15 throw redirect(303, '/')
_15 const { data: tableData } = await supabase.from('test').select('*')
_40 import { fail, redirect } from '@sveltejs/kit'
_40 import { AuthApiError } from '@supabase/supabase-js'
_40 export const actions = {
_40 signin: async ({ request, locals: { supabase } }) => {
_40 const formData = await request.formData()
_40 const email = formData.get('email') as string
_40 const password = formData.get('password') as string
_40 const { error } = await supabase.auth.signInWithPassword({
_40 if (error instanceof AuthApiError && error.status === 400) {
_40 error: 'Invalid credentials.',
_40 error: 'Server error. Try again later.',
_40 throw redirect(303, '/dashboard')
_40 signout: async ({ locals: { supabase } }) => {
_40 await supabase.auth.signOut()
_40 throw redirect(303, '/')
Proof Key for Code Exchange (PKCE) is the new server-side auth flow implemented by the SvelteKit Auth Helpers. It requires a server endpoint for /auth/callback
that exchanges an auth code
for the user's session
.
Check the Code Exchange Route steps above to implement this server endpoint.
For authentication methods that have a redirectTo
or emailRedirectTo
, this must be set to this new code exchange route handler - /auth/callback
. This is an example with the signUp
function:
_10 await supabase.auth.signUp({
_10 email: 'jon@example.com',
_10 password: 'sup3rs3cur3',
_10 emailRedirectTo: 'http://localhost:3000/auth/callback',
In version 0.9 we now setup our Supabase client for the server inside of a hooks.server.ts
file.
0.8.x 0.9.0
_10 import { createClient } from '@supabase/auth-helpers-sveltekit'
_10 import { env } from '$env/dynamic/public'
_10 // or use the static env
_10 // import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
_10 export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
In order to use the Supabase library in your client code you will need to setup a shared load function inside the root +layout.ts
and create a +layout.svelte
to handle our event listening for Auth events.
0.8.x 0.9.0
src/routes/ +layout.svelte
_20 <!-- src/routes/+layout.svelte -->
_20 import { supabaseClient } from '$lib/db'
_20 import { invalidate } from '$app/navigation'
_20 import { onMount } from 'svelte'
_20 data: { subscription },
_20 } = supabaseClient.auth.onAuthStateChange(() => {
_20 invalidate('supabase:auth')
_20 subscription.unsubscribe()
Since version 0.9 relies on hooks.server.ts
to setup our client, we no longer need the hooks.client.ts
in our project for Supabase related code.
0.8.x 0.9.0
_19 /// <reference types="@sveltejs/kit" />
_19 // See https://kit.svelte.dev/docs/types#app
_19 // for information about these interfaces
_19 // and what to do when importing types
_19 declare namespace App {
_19 Database: import('./DatabaseDefinitions').Database
_19 // interface Locals {}
_19 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
_19 // interface Error {}
_19 // interface Platform {}
Protecting a page #
0.8.x 0.9.0
src/routes/profile/ +page.svelte
_10 <!-- src/routes/profile/+page.svelte -->
_10 /** @type {import('./$types').PageData} */
_10 $: ({ user, tableData } = data)
_10 <div>Protected content for {user.email}</div>
_10 <pre>{JSON.stringify(tableData, null, 2)}</pre>
_10 <pre>{JSON.stringify(user, null, 2)}</pre>
src/routes/profile/ +page.ts
_17 // src/routes/profile/+page.ts
_17 import type { PageLoad } from './$types'
_17 import { getSupabase } from '@supabase/auth-helpers-sveltekit'
_17 import { redirect } from '@sveltejs/kit'
_17 export const load: PageLoad = async (event) => {
_17 const { session, supabaseClient } = await getSupabase(event)
_17 throw redirect(303, '/')
_17 const { data: tableData } = await supabaseClient.from('test').select('*')
0.8.x 0.9.0
src/routes/api/protected-route/ +server.ts
_14 // src/routes/api/protected-route/+server.ts
_14 import type { RequestHandler } from './$types'
_14 import { getSupabase } from '@supabase/auth-helpers-sveltekit'
_14 import { json, redirect } from '@sveltejs/kit'
_14 export const GET: RequestHandler = async (event) => {
_14 const { session, supabaseClient } = await getSupabase(event)
_14 throw redirect(303, '/')
_14 const { data } = await supabaseClient.from('test').select('*')
_14 return json({ data })
0.7.x 0.8.0
_19 import { createClient } from '@supabase/supabase-js'
_19 import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
_19 import { dev } from '$app/environment'
_19 import { env } from '$env/dynamic/public'
_19 // or use the static env
_19 // import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
_19 export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
_19 persistSession: false,
_19 autoRefreshToken: false,
_19 setupSupabaseHelpers({
0.7.x 0.8.0
src/routes/ +layout.svelte
_15 // make sure the supabase instance is initialized on the client
_15 import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit'
_15 import { page } from '$app/stores'
_15 import { invalidateAll } from '$app/navigation'
_15 // this sets up automatic token refreshing
_15 startSupabaseSessionSync({
_15 handleRefresh: () => invalidateAll(),
0.7.x 0.8.0
_10 // make sure the supabase instance is initialized on the server
_10 import { dev } from '$app/environment'
_10 import { auth } from '@supabase/auth-helpers-sveltekit/server'
_10 export const handle = auth()
Optional if using additional handle methods
_10 // make sure the supabase instance is initialized on the server
_10 import { dev } from '$app/environment'
_10 import { auth } from '@supabase/auth-helpers-sveltekit/server'
_10 import { sequence } from '@sveltejs/kit/hooks'
_10 export const handle = sequence(auth(), yourHandler)
0.7.x 0.8.0
_17 /// <reference types="@sveltejs/kit" />
_17 // See https://kit.svelte.dev/docs/types#app
_17 // for information about these interfaces
_17 // and what to do when importing types
_17 declare namespace App {
_17 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
_17 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
_17 // interface Error {}
_17 // interface Platform {}
withPageAuth
#
0.7.x 0.8.0
src/routes/protected-route/ +page.svelte
_12 import type { PageData } from './$types'
_12 export let data: PageData
_12 $: ({ tableData, user } = data)
_12 <div>Protected content for {user.email}</div>
_12 <p>server-side fetched data with RLS:</p>
_12 <pre>{JSON.stringify(tableData, null, 2)}</pre>
_12 <pre>{JSON.stringify(user, null, 2)}</pre>
src/routes/protected-route/ +page.ts
_12 import { withAuth } from '@supabase/auth-helpers-sveltekit'
_12 import { redirect } from '@sveltejs/kit'
_12 import type { PageLoad } from './$types'
_12 export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
_12 throw redirect(303, '/')
_12 const { data: tableData } = await getSupabaseClient().from('test').select('*')
_12 return { tableData, user: session.user }
0.7.x 0.8.0
src/routes/api/protected-route/ +server.ts
_18 import type { RequestHandler } from './$types'
_18 import { withAuth } from '@supabase/auth-helpers-sveltekit'
_18 import { json, redirect } from '@sveltejs/kit'
_18 export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
_18 throw redirect(303, '/')
_18 const { data } = await getSupabaseClient().from<TestTable>('test').select('*')
_18 return json({ data })
There are numerous breaking changes in the latest 0.7.0 version of this library.
The environment variable prefix is now PUBLIC_
instead of VITE_
(e.g., VITE_SUPABASE_URL
is now PUBLIC_SUPABASE_URL
).
0.6.11 and below 0.7.0
_10 import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit';
_10 const { supabaseClient } = createSupabaseClient(
_10 import.meta.env.VITE_SUPABASE_URL as string,
_10 import.meta.env.VITE_SUPABASE_ANON_KEY as string
_10 export { supabaseClient };
0.6.11 and below 0.7.0
src/routes/ __layout.svelte
_10 import { session } from '$app/stores'
_10 import { supabaseClient } from '$lib/db'
_10 import { SupaAuthHelper } from '@supabase/auth-helpers-svelte'
_10 <SupaAuthHelper {supabaseClient} {session}>
0.6.11 and below 0.7.0
_14 import { handleAuth } from '@supabase/auth-helpers-sveltekit'
_14 import type { GetSession, Handle } from '@sveltejs/kit'
_14 import { sequence } from '@sveltejs/kit/hooks'
_14 export const handle: Handle = sequence(...handleAuth())
_14 export const getSession: GetSession = async (event) => {
_14 const { user, accessToken, error } = event.locals
0.6.11 and below 0.7.0
_18 /// <reference types="@sveltejs/kit" />
_18 // See https://kit.svelte.dev/docs/types#app
_18 // for information about these interfaces
_18 declare namespace App {
_18 interface UserSession {
_18 user: import('@supabase/supabase-js').User
_18 interface Locals extends UserSession {
_18 error: import('@supabase/supabase-js').ApiError
_18 interface Session extends UserSession {}
_18 // interface Platform {}
_18 // interface Stuff {}
0.6.11 and below 0.7.0
_10 import { session } from '$app/stores'
_10 <h1>I am not logged in</h1>
_10 <h1>Welcome {$session.user.email}</h1>
_10 <p>I am logged in!</p>
withPageAuth
#
0.6.11 and below 0.7.0
src/routes/ protected-route.svelte
_27 <script lang="ts" context="module">
_27 import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit'
_27 import type { Load } from './__types/protected-page'
_27 export const load: Load = async ({ session }) =>
_27 const { data } = await supabaseServerClient(session.accessToken).from('test').select('*')
_27 return { props: { data, user: session.user } }
_27 <div>Protected content for {user.email}</div>
_27 <p>server-side fetched data with RLS:</p>
_27 <pre>{JSON.stringify(data, null, 2)}</pre>
_27 <pre>{JSON.stringify(user, null, 2)}</pre>
0.6.11 and below 0.7.0
src/routes/api/ protected-route.ts
_22 import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit'
_22 import type { RequestHandler } from './__types/protected-route'
_22 export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
_22 withApiAuth({ user: locals.user }, async () => {
_22 // Run queries with RLS on the server
_22 const { data } = await supabaseServerClient(request).from('test').select('*')