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
.
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:
# Find these in your Supabase project settings https://supabase.com/dashboard/project/_/settings/api
PUBLIC_SUPABASE_URL = https://your-project.supabase.co
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:
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
export const handle = async ({ event , resolve }) => {
event.locals.supabase = createSupabaseServerClient ({
supabaseUrl: PUBLIC_SUPABASE_URL ,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY ,
* a little helper that is written for convenience so that instead
* of calling `const { data: { session } } = await supabase.auth.getSession()`
* you just call this `await getSession()`
event.locals. getSession = async () => {
} = await event.locals.supabase.auth. getSession ()
filterSerializedResponseHeaders ( name ) {
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
import { redirect } from '@sveltejs/kit'
export const GET = async ({ url , locals : { supabase } }) => {
const code = url.searchParams. get ( 'code' )
await supabase.auth. exchangeCodeForSession (code)
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.
import { SupabaseClient, Session } from '@supabase/supabase-js'
import { Database } from './DatabaseDefinitions'
supabase : SupabaseClient < Database >
getSession () : Promise < Session | null >
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
// src/routes/+layout.server.js
export const load = async ({ locals : { getSession } }) => {
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
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit'
export const load = async ({ fetch , data , depends }) => {
const supabase = createSupabaseLoadClient ({
supabaseUrl: PUBLIC_SUPABASE_URL ,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY ,
serverSession: data.session,
} = await supabase.auth. getSession ()
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
<!-- src/routes/+layout.svelte -->
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
let { supabase, session } = data
$ : ({ supabase, session } = data)
} = supabase.auth. onAuthStateChange (( event , _session ) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate ( 'supabase:auth' )
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
<!-- // src/routes/auth/+page.svelte -->
const handleSignUp = async () => {
await supabase.auth. signUp ({
emailRedirectTo: `${ location . origin }/auth/callback` ,
const handleSignIn = async () => {
await supabase.auth. signInWithPassword ({
const handleSignOut = async () => {
await supabase.auth. signOut ()
< form on : submit = " { handleSignUp } " >
< input name = "email" bind : value = " { email } " />
< input type = "password" name = "password" bind : value = " { password } " />
< button on : click = " { handleSignIn } " >Sign in</ button >
< 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
// src/routes/login/+page.server.js
import { fail } from '@sveltejs/kit'
default : async ({ request , url , locals : { supabase } }) => {
const formData = await request. formData ()
const email = formData. get ( 'email' )
const password = formData. get ( 'password' )
const { error } = await supabase.auth. signUp ({
emailRedirectTo: `${ url . origin }/auth/callback` ,
return fail ( 500 , { message: 'Server error. Try again later.' , success: false , email })
message: 'Please check your email for a magic link to log into the website.' ,
src/routes/login/ +page.svelte
<!-- // src/routes/login/+page.svelte -->
import { enhance } from '$app/forms'
< form method = "post" use : enhance >
< input name = "email" value = { form?.email ?? '' } />
< input type = "password" name = "password" />
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
// src/routes/api/protected-route/+server.ts
import { json, error } from '@sveltejs/kit'
export const GET = async ({ locals : { supabase , getSession } }) => {
const session = await getSession ()
// the user is not signed in
throw error ( 401 , { message: 'Unauthorized' })
const { data } = await supabase. from ( 'test' ). select ( '*' )
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
// src/routes/posts/+page.server.ts
import { error, fail } from '@sveltejs/kit'
createPost : async ({ request , locals : { supabase , getSession } }) => {
const session = await getSession ()
// the user is not signed in
throw error ( 401 , { message: 'Unauthorized' })
// we are save, let the user create the post
const formData = await request. formData ()
const content = formData. get ( 'content' )
const { error : createPostError , data : newPost } = await supabase
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
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
import { redirect, error } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
async function supabase ({ event , resolve }) {
event.locals.supabase = createSupabaseServerClient ({
supabaseUrl: PUBLIC_SUPABASE_URL ,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY ,
* a little helper that is written for convenience so that instead
* of calling `const { data: { session } } = await supabase.auth.getSession()`
* you just call this `await getSession()`
event.locals. getSession = async () => {
} = await event.locals.supabase.auth. getSession ()
filterSerializedResponseHeaders ( name ) {
return name === 'content-range'
async function authorization ({ event , resolve }) {
// protect requests to all routes that start with /protected-routes
if (event.url.pathname. startsWith ( '/protected-routes' )) {
const session = await event.locals. getSession ()
// the user is not signed in
// protect POST requests to all routes that start with /protected-posts
if (event.url.pathname. startsWith ( '/protected-posts' ) && event.request.method === 'POST' ) {
const session = await event.locals. getSession ()
// the user is not signed in
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:
async function loadData () {
const { data : result } = await data.supabase. from ( 'test' ). select ( '*' ). limit ( 20 )
< p >client-side data fetching with RLS</ p >
< pre > { JSON . stringify (loadedData, null , 2 ) } </ pre >
src/routes/profile/ +page.svelte
<!-- src/routes/profile/+page.svelte -->
let { user, tableData } = data
$ : ({ user, tableData } = data)
< div >Protected content for { user.email } </ div >
< pre > { JSON . stringify (tableData, null , 2 ) } </ pre >
< pre > { JSON . stringify (user, null , 2 ) } </ pre >
src/routes/profile/ +page.ts
// src/routes/profile/+page.ts
import { redirect } from '@sveltejs/kit'
export const load = async ({ parent }) => {
const { supabase , session } = await parent ()
const { data : tableData } = await supabase. from ( 'test' ). select ( '*' )
import { fail, redirect } from '@sveltejs/kit'
import { AuthApiError } from '@supabase/supabase-js'
signin : 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 ({
if (error instanceof AuthApiError && error.status === 400 ) {
error: 'Invalid credentials.' ,
error: 'Server error. Try again later.' ,
throw redirect ( 303 , '/dashboard' )
signout : async ({ locals : { supabase } }) => {
await supabase.auth. signOut ()
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:
await supabase.auth. signUp ({
email: 'jon@example.com' ,
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
import { createClient } from '@supabase/auth-helpers-sveltekit'
import { env } from '$env/dynamic/public'
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
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
<!-- src/routes/+layout.svelte -->
import { supabaseClient } from '$lib/db'
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
} = supabaseClient.auth. onAuthStateChange (() => {
invalidate ( 'supabase:auth' )
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
/// < reference types = "@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
Database : import ( './DatabaseDefinitions' ). Database
session : import ( '@supabase/auth-helpers-sveltekit' ). SupabaseSession
Protecting a page #
0.8.x 0.9.0
src/routes/profile/ +page.svelte
<!-- src/routes/profile/+page.svelte -->
/** @type {import('./$types').PageData} */
$ : ({ user, tableData } = data)
< div >Protected content for { user.email } </ div >
< pre > { JSON . stringify (tableData, null , 2 ) } </ pre >
< pre > { JSON . stringify (user, null , 2 ) } </ pre >
src/routes/profile/ +page.ts
// src/routes/profile/+page.ts
import type { PageLoad } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { redirect } from '@sveltejs/kit'
export const load : PageLoad = async ( event ) => {
const { session , supabaseClient } = await getSupabase (event)
const { data : tableData } = await supabaseClient. from ( 'test' ). select ( '*' )
0.8.x 0.9.0
src/routes/api/protected-route/ +server.ts
// src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { json, redirect } from '@sveltejs/kit'
export const GET : RequestHandler = async ( event ) => {
const { session , supabaseClient } = await getSupabase (event)
const { data } = await supabaseClient. from ( 'test' ). select ( '*' )
0.7.x 0.8.0
import { createClient } from '@supabase/supabase-js'
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
import { dev } from '$app/environment'
import { env } from '$env/dynamic/public'
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabaseClient = createClient (env. PUBLIC_SUPABASE_URL , env. PUBLIC_SUPABASE_ANON_KEY , {
0.7.x 0.8.0
src/routes/ +layout.svelte
// make sure the supabase instance is initialized on the client
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit'
import { page } from '$app/stores'
import { invalidateAll } from '$app/navigation'
// this sets up automatic token refreshing
startSupabaseSessionSync ({
handleRefresh : () => invalidateAll (),
0.7.x 0.8.0
// make sure the supabase instance is initialized on the server
import { dev } from '$app/environment'
import { auth } from '@supabase/auth-helpers-sveltekit/server'
export const handle = auth ()
Optional if using additional handle methods
// make sure the supabase instance is initialized on the server
import { dev } from '$app/environment'
import { auth } from '@supabase/auth-helpers-sveltekit/server'
import { sequence } from '@sveltejs/kit/hooks'
export const handle = sequence ( auth (), yourHandler)
0.7.x 0.8.0
/// < reference types = "@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
session : import ( '@supabase/auth-helpers-sveltekit' ). SupabaseSession
session : import ( '@supabase/auth-helpers-sveltekit' ). SupabaseSession
withPageAuth #
0.7.x 0.8.0
src/routes/protected-route/ +page.svelte
import type { PageData } from './$types'
export let data : PageData
$ : ({ tableData , user } = data )
< div >Protected content for {user.email}</div>
< p >server-side fetched data with RLS:</ p >
< pre > { JSON . stringify (tableData, null , 2 ) } </ pre >
< pre > { JSON . stringify (user, null , 2 ) } </ pre >
src/routes/protected-route/ +page.ts
import { withAuth } from '@supabase/auth-helpers-sveltekit'
import { redirect } from '@sveltejs/kit'
import type { PageLoad } from './$types'
export const load : PageLoad = withAuth ( async ({ session , getSupabaseClient }) => {
const { data : tableData } = await getSupabaseClient (). from ( 'test' ). select ( '*' )
return { tableData, user: session.user }
0.7.x 0.8.0
src/routes/api/protected-route/ +server.ts
import type { RequestHandler } from './$types'
import { withAuth } from '@supabase/auth-helpers-sveltekit'
import { json, redirect } from '@sveltejs/kit'
export const GET : RequestHandler = withAuth ( async ({ session , getSupabaseClient }) => {
const { data } = await getSupabaseClient (). from < TestTable >( 'test' ). select ( '*' )
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
import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit' ;
const { supabaseClient } = createSupabaseClient (
import . meta .env. VITE_SUPABASE_URL as string ,
import . meta .env. VITE_SUPABASE_ANON_KEY as string
export { supabaseClient };
0.6.11 and below 0.7.0
src/routes/ __layout.svelte
import { session } from '$app/stores'
import { supabaseClient } from '$lib/db'
import { SupaAuthHelper } from '@supabase/auth-helpers-svelte'
< SupaAuthHelper { supabaseClient } { session } >
0.6.11 and below 0.7.0
import { handleAuth } from '@supabase/auth-helpers-sveltekit'
import type { GetSession, Handle } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
export const handle : Handle = sequence ( ... handleAuth ())
export const getSession : GetSession = async ( event ) => {
const { user , accessToken , error } = event.locals
0.6.11 and below 0.7.0
/// < reference types = "@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
user : import ( '@supabase/supabase-js' ). User
interface Locals extends UserSession {
error : import ( '@supabase/supabase-js' ). ApiError
interface Session extends UserSession {}
0.6.11 and below 0.7.0
import { session } from '$app/stores'
< h1 >I am not logged in</ h1 >
< h1 >Welcome { $session.user.email } </ h1 >
withPageAuth#
0.6.11 and below 0.7.0
src/routes/ protected-route.svelte
< script lang = "ts" context = "module" >
import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit'
import type { Load } from './__types/protected-page'
export const load : Load = async ({ session }) =>
const { data } = await supabaseServerClient (session.accessToken). from ( 'test' ). select ( '*' )
return { props: { data, user: session.user } }
< div >Protected content for { user.email } </ div >
< p >server-side fetched data with RLS:</ p >
< pre > { JSON . stringify (data, null , 2 ) } </ pre >
< pre > { JSON . stringify (user, null , 2 ) } </ pre >
0.6.11 and below 0.7.0
src/routes/api/ protected-route.ts
import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit'
import type { RequestHandler } from './__types/protected-route'
export const GET : RequestHandler < GetOutput > = async ({ locals , request }) =>
withApiAuth ({ user: locals.user }, async () => {
// Run queries with RLS on the server
const { data } = await supabaseServerClient (request). from ( 'test' ). select ( '*' )