Auth

Supabase Auth with SvelteKit

This submodule provides convenience helpers for implementing user authentication in SvelteKit applications.

Configuration#

Install SvelteKit Auth Helpers library#

This library supports Node.js ^16.15.0.

Terminal
npm install @supabase/auth-helpers-sveltekit @supabase/supabase-js

Declare Environment Variables#

Retrieve your project's URL and anon key from your API settings, and create a .env.local file with the following environment variables:

.env.local
# 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

Creating a Supabase Client#

Create a new hooks.server.js file in the root of your project and populate with the following:

src/hooks.server.js
// src/hooks.server.js
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,
event,
})

/**
* 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 () => {
const {
data: { session },
} = await event.locals.supabase.auth.getSession()
return session
}

return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
})
}

Code Exchange Route#

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.

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')

if (code) {
await supabase.auth.exchangeCodeForSession(code)
}

throw redirect(303, '/')
}

Generate types from your database#

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.

src/app.d.ts
// src/app.d.ts

import { SupabaseClient, Session } from '@supabase/supabase-js'
import { Database } from './DatabaseDefinitions'

declare global {
namespace App {
interface Locals {
supabase: SupabaseClient<Database>
getSession(): Promise<Session | null>
}
interface PageData {
session: Session | null
}
// interface Error {}
// interface Platform {}
}
}

Authentication#

Authentication can be initiated client or server-side. All of the supabase-js authentication strategies are supported with the Auth Helpers client.

Client-side#

Send session to client#

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.

src/routes/+layout.server.js
// src/routes/+layout.server.js
export const load = async ({ locals: { getSession } }) => {
return {
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.

src/routes/+layout.js
// src/routes/+layout.js
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 }) => {
depends('supabase:auth')

const supabase = createSupabaseLoadClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event: { fetch },
serverSession: data.session,
})

const {
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.

Setting up the event listener on the client side#

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 -->
<script lang="ts">
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'

export let data

let { supabase, session } = data
$: ({ supabase, session } = data)

onMount(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
})

return () => subscription.unsubscribe()
});
</script>

<slot />

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.

Sign in / Sign up / Sign out#

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 -->
<script>
export let data
let { supabase } = data
$: ({ supabase } = data)

let email
let password

const handleSignUp = async () => {
await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${location.origin}/auth/callback`,
},
})
}

const handleSignIn = async () => {
await supabase.auth.signInWithPassword({
email,
password,
})
}

const handleSignOut = async () => {
await supabase.auth.signOut()
}
</script>

<form on:submit="{handleSignUp}">
<input name="email" bind:value="{email}" />
<input type="password" name="password" bind:value="{password}" />
<button>Sign up</button>
</form>

<button on:click="{handleSignIn}">Sign in</button>
<button on:click="{handleSignOut}">Sign out</button>

Server-side#

Form Actions can be used to trigger the authentication process from form submissions.

src/routes/login/+page.server.js
// src/routes/login/+page.server.js
import { fail } from '@sveltejs/kit'

export const actions = {
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({
email,
password,
options: {
emailRedirectTo: `${url.origin}/auth/callback`,
},
})

if (error) {
return fail(500, { message: 'Server error. Try again later.', success: false, email })
}

return {
message: 'Please check your email for a magic link to log into the website.',
success: true,
}
},
}
src/routes/login/+page.svelte
<!-- // src/routes/login/+page.svelte -->
<script>
import { enhance } from '$app/forms'
export let form
</script>

<form method="post" use:enhance>
<input name="email" value={form?.email ?? ''} />
<input type="password" name="password" />
<button>Sign up</button>
</form>

Authorization#

Protecting API routes#

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()
if (!session) {
// the user is not signed in
throw error(401, { message: 'Unauthorized' })
}
const { data } = await supabase.from('test').select('*')

return json({ data })
}

If you visit /api/protected-route without a valid session cookie, you will get a 401 response.

Protecting Actions#

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'

export const actions = {
createPost: async ({ request, locals: { supabase, getSession } }) => {
const session = await getSession()

if (!session) {
// 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
.from('posts')
.insert({ content })

if (createPostError) {
return fail(500, {
supabaseErrorMessage: createPostError.message,
})
}
return {
newPost,
}
},
}

If you try to submit a form with the action ?/createPost without a valid session cookie, you will get a 401 error response.

Protecting multiple routes#

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:

src/hooks.server.js
// src/hooks.server.js
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,
event,
})

/**
* 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 () => {
const {
data: { session },
} = await event.locals.supabase.auth.getSession()
return session
}

return resolve(event, {
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()
if (!session) {
// the user is not signed in
throw redirect(303, '/')
}
}

// 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()
if (!session) {
// the user is not signed in
throw error(303, '/')
}
}

return resolve(event)
}

export const handle = sequence(supabase, authorization)

Data fetching#

Client-side data fetching with RLS#

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:

src/routes/+page.svelte
<script lang="ts">
export let data

let loadedData = []
async function loadData() {
const { data: result } = await data.supabase.from('test').select('*').limit(20)
loadedData = result
}

$: if (data.session) {
loadData()
}
</script>

{#if data.session}
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/if}

Server-side data fetching with RLS#

src/routes/profile/+page.svelte
<!-- src/routes/profile/+page.svelte -->
<script lang="ts">
export let data

let { user, tableData } = data
$: ({ user, tableData } = data)
</script>

<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()
if (!session) {
throw redirect(303, '/')
}
const { data: tableData } = await supabase.from('test').select('*')

return {
user: session.user,
tableData,
}
}

Saving and deleting the session#

import { fail, redirect } from '@sveltejs/kit'
import { AuthApiError } from '@supabase/supabase-js'

export const actions = {
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({
email,
password,
})

if (error) {
if (error instanceof AuthApiError && error.status === 400) {
return fail(400, {
error: 'Invalid credentials.',
values: {
email,
},
})
}
return fail(500, {
error: 'Server error. Try again later.',
values: {
email,
},
})
}

throw redirect(303, '/dashboard')
},

signout: async ({ locals: { supabase } }) => {
await supabase.auth.signOut()
throw redirect(303, '/')
},
}

Migration Guide #

Migrate to 0.10#

PKCE Auth Flow#

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.

Authentication#

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',
password: 'sup3rs3cur3',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback',
},
})

Migrate from 0.8.x to 0.9 #

Set up the Supabase client #

In version 0.9 we now setup our Supabase client for the server inside of a hooks.server.ts file.

src/lib/db.ts
// src/lib/db.ts
import { createClient } from '@supabase/auth-helpers-sveltekit'
import { env } from '$env/dynamic/public'
// or use the static env

// 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)

Initialize the client #

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.

src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { supabaseClient } from '$lib/db'
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'

onMount(() => {
const {
data: { subscription },
} = supabaseClient.auth.onAuthStateChange(() => {
invalidate('supabase:auth')
})

return () => {
subscription.unsubscribe()
}
})
</script>

<slot />

Set up hooks #

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.

Typings #

src/app.d.ts
// src/app.d.ts
/// <reference types="@sveltejs/kit" />

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Supabase {
Database: import('./DatabaseDefinitions').Database
SchemaName: 'public'
}

// interface Locals {}
interface PageData {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
}
// interface Error {}
// interface Platform {}
}

Protecting a page #

src/routes/profile/+page.svelte
<!-- src/routes/profile/+page.svelte -->
<script lang="ts">
/** @type {import('./$types').PageData} */
export let data
$: ({ user, tableData } = data)
</script>

<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)
if (!session) {
throw redirect(303, '/')
}
const { data: tableData } = await supabaseClient.from('test').select('*')

return {
user: session.user,
tableData,
}
}

Protecting a API route #

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)
if (!session) {
throw redirect(303, '/')
}
const { data } = await supabaseClient.from('test').select('*')

return json({ data })
}

Migrate from 0.7.x to 0.8 #

Set up the Supabase client #

src/lib/db.ts
import { createClient } from '@supabase/supabase-js'
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
import { dev } from '$app/environment'
import { env } from '$env/dynamic/public'
// or use the static env

// 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, {
persistSession: false,
autoRefreshToken: false,
})

setupSupabaseHelpers({
supabaseClient,
cookieOptions: {
secure: !dev,
},
})

Initialize the client #

src/routes/+layout.svelte
<script lang="ts">
// make sure the supabase instance is initialized on the client
import '$lib/db'
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit'
import { page } from '$app/stores'
import { invalidateAll } from '$app/navigation'

// this sets up automatic token refreshing
startSupabaseSessionSync({
page,
handleRefresh: () => invalidateAll(),
})
</script>

<slot />

Set up hooks #

src/hooks.server.ts
// make sure the supabase instance is initialized on the server
import '$lib/db'
import { dev } from '$app/environment'
import { auth } from '@supabase/auth-helpers-sveltekit/server'

export const handle = auth()

Optional if using additional handle methods

src/hooks.server.ts
// make sure the supabase instance is initialized on the server
import '$lib/db'
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)

Typings #

src/app.d.ts
/// <reference types="@sveltejs/kit" />

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Locals {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
}

interface PageData {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
}

// interface Error {}
// interface Platform {}
}

withPageAuth #

src/routes/protected-route/+page.svelte
<script lang="ts">
import type { PageData } from './$types'

export let data: PageData
$: ({ tableData, user } = data)
</script>

<div>Protected content for {user.email}</div>
<p>server-side fetched data with RLS:</p>
<pre>{JSON.stringify(tableData, null, 2)}</pre>
<p>user:</p>
<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 }) => {
if (!session.user) {
throw redirect(303, '/')
}

const { data: tableData } = await getSupabaseClient().from('test').select('*')
return { tableData, user: session.user }
})

withApiAuth #

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'

interface TestTable {
id: string
created_at: string
}

export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
throw redirect(303, '/')
}

const { data } = await getSupabaseClient().from<TestTable>('test').select('*')

return json({ data })
})

Migrate from 0.6.11 and below to 0.7.0 #

There are numerous breaking changes in the latest 0.7.0 version of this library.

Environment variable prefix#

The environment variable prefix is now PUBLIC_ instead of VITE_ (e.g., VITE_SUPABASE_URL is now PUBLIC_SUPABASE_URL).

Set up the Supabase client #

src/lib/db.ts
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 };

Initialize the client #

src/routes/__layout.svelte
<script>
import { session } from '$app/stores'
import { supabaseClient } from '$lib/db'
import { SupaAuthHelper } from '@supabase/auth-helpers-svelte'
</script>

<SupaAuthHelper {supabaseClient} {session}>
<slot />
</SupaAuthHelper>

Set up hooks #

src/hooks.ts
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
return {
user,
accessToken,
error,
}
}

Typings #

src/app.d.ts
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface UserSession {
user: import('@supabase/supabase-js').User
accessToken?: string
}

interface Locals extends UserSession {
error: import('@supabase/supabase-js').ApiError
}

interface Session extends UserSession {}

// interface Platform {}
// interface Stuff {}
}

Check the user on the client#

src/routes/index.svelte
<script>
import { session } from '$app/stores'
</script>

{#if !$session.user}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {$session.user.email}</h1>
<p>I am logged in!</p>
{/if}

withPageAuth#

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 }) =>
withPageAuth(
{
redirectTo: '/',
user: session.user,
},
async () => {
const { data } = await supabaseServerClient(session.accessToken).from('test').select('*')
return { props: { data, user: session.user } }
}
)
</script>

<script>
export let data
export let user
</script>

<div>Protected content for {user.email}</div>
<p>server-side fetched data with RLS:</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>

withApiAuth#

src/routes/api/protected-route.ts
import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit'
import type { RequestHandler } from './__types/protected-route'

interface TestTable {
id: string
created_at: string
}

interface GetOutput {
data: TestTable[]
}

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('*')

return {
status: 200,
body: { data },
}
})

We only collect analytics essential to ensuring smooth operation of our services.

Learn more