Supabase Auth with SvelteKit
This submodule provides convenience helpers for implementing user authentication in SvelteKit applications.
Installation#
This library supports Node.js ^16.15.0
.
npm install @supabase/auth-helpers-sveltekit
Getting Started#
Configuration#
Set up the following env vars. For local development you can set them in a .env
file. See an example.
1# Find these in your Supabase project settings > API 2PUBLIC_SUPABASE_URL=https://your-project.supabase.co 3PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Set up the Supabase client#
Start off by creating a db.ts
file inside of the src/lib
directory and instantiate the supabaseClient
.
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)
To make sure the client is initialized on the server and the client, include this file in src/hooks.server.js
and src/hooks.client.js
:
import '$lib/db'
Synchronizing the page store#
Edit your +layout.svelte
file and set up the client side.
<script> 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 />
Every PageLoad
or LayoutLoad
using getSupabase()
will update when invalidate('supabase:auth')
is called.
If some data is not updated on signin/signout you can fall back to invalidateAll()
.
Send session to client#
To make the session available to the UI (pages, layouts), pass the session in the root layout server load function:
import type { LayoutServerLoad } from './$types'
import { getServerSession } from '@supabase/auth-helpers-sveltekit'
export const load: LayoutServerLoad = async (event) => {
return {
session: await getServerSession(event),
}
}
In addition you can create a layout load function if you are using invalidate('supabase:auth')
:
import type { LayoutLoad } from './$types' import { getSupabase } from '@supabase/auth-helpers-sveltekit' export const load: LayoutLoad = async (event) => { const { session } = await getSupabase(event) return { session } }
This results in fewer server calls as the client manages the session on its own.
Typings#
In order to get the most out of TypeScript and it´s intellisense, you should import our types into the app.d.ts
type definition file that comes with your SvelteKit project.
/// <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/supabase-js').Session | null
}
// interface Error {}
// interface Platform {}
}
Basic Setup#
You can now determine if a user is authenticated on the client-side by checking that the session
object in $page.data
is defined.
<script> import { page } from '$app/stores' </script> {#if !$page.data.session} <h1>I am not logged in</h1> {:else} <h1>Welcome {$page.data.session.user.email}</h1> <p>I am logged in!</p> {/if}
Client-side data fetching with RLS#
For row level security to work properly when fetching data client-side, you need to make sure to import the { supabaseClient }
from $lib/db
and only run your query once the session is defined client-side in $page.data
:
<script> import { supabaseClient } from '$lib/db' import { page } from '$app/stores' let loadedData = [] async function loadData() { const { data } = await supabaseClient.from('test').select('*').limit(20) loadedData = data } $: if ($page.data.session) { loadData() } </script> {#if $page.data.session} <p>client-side data fetching with RLS</p> <pre>{JSON.stringify(loadedData, null, 2)}</pre> {/if}
Server-side data fetching with RLS#
<script> /** @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>
For row level security to work in a server environment, you need to use the getSupabase
helper to check if the user is authenticated. The helper requires the event
and returns session
and supabaseClient
:
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 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
.
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 })
}
If you visit /api/protected-route
without a valid session cookie, you will get a 303 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
.
import type { Actions } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { error, invalid } from '@sveltejs/kit'
export const actions: Actions = {
createPost: async (event) => {
const { request } = event
const { session, supabaseClient } = await getSupabase(event)
if (!session) {
// the user is not signed in
throw error(403, { 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 supabaseClient
.from('posts')
.insert({ content })
if (createPostError) {
return invalid(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 403 error response.
Saving and deleting the session#
import type { Actions } from './$types'
import { invalid, redirect } from '@sveltejs/kit'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { AuthApiError } from '@supabase/supabase-js'
export const actions: Actions = {
signin: async (event) => {
const { request, cookies, url } = event
const { session, supabaseClient } = await getSupabase(event)
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabaseClient.auth.signInWithPassword({
email,
password,
})
if (error) {
if (error instanceof AuthApiError && error.status === 400) {
return invalid(400, {
error: 'Invalid credentials.',
values: {
email,
},
})
}
return invalid(500, {
error: 'Server error. Try again later.',
values: {
email,
},
})
}
throw redirect(303, '/dashboard')
},
signout: async (event) => {
const { supabaseClient } = await getSupabase(event)
await supabaseClient.auth.signOut()
throw redirect(303, '/')
},
}
Protecting multiple routes#
To avoid writing the same auth logic in every single route you can use the handle hook to protect multiple routes at once.
import type { RequestHandler } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { redirect, error } from '@sveltejs/kit'
export const handle: Handle = async ({ event, resolve }) => {
// protect requests to all routes that start with /protected-routes
if (event.url.pathname.startsWith('/protected-routes')) {
const { session, supabaseClient } = await getSupabase(event)
if (!session) {
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, supabaseClient } = await getSupabase(event)
if (!session) {
throw error(303, '/')
}
}
return resolve(event)
}
Migrate from 0.7.x to 0.8 [#migration]#
Set up the Supabase client [#migration-set-up-supabase-client]#
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 [#migration-initialize-client]#
<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 [#migration-set-up-hooks]#
// 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
// 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 [#migration-typings]#
/// <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 [#migration-with-page-auth]#
<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>
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 [#migration-with-api-auth]#
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 [#migration-0-7]#
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 [#migration-set-up-supabase-client-0-7]#
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 [#migration-initialize-client-0-7]#
<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 [#migration-set-up-hooks-0-7]#
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 [#migration-typings-0-7]#
/// <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#
<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#
<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#
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 },
}
})