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
.
1npm install @supabase/auth-helpers-sveltekit
Getting Started#
Configuration#
Set up the fillowing env vars. For local development you can set them in a .env
file. See an example.
# Find these in your Supabase project settings > API PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_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
.
src/lib/db.ts1import { createClient } from '@supabase/auth-helpers-sveltekit' 2import { env } from '$env/dynamic/public' 3// or use the static env 4// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; 5 6export 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
:
1import '$lib/db'
Synchronizing the page store#
Edit your +layout.svelte
file and set up the client side.
src/routes/+layout.svelte1<script> 2 import { supabaseClient } from '$lib/db' 3 import { invalidate } from '$app/navigation' 4 import { onMount } from 'svelte' 5 6 onMount(() => { 7 const { 8 data: { subscription }, 9 } = supabaseClient.auth.onAuthStateChange(() => { 10 invalidate('supabase:auth') 11 }) 12 13 return () => { 14 subscription.unsubscribe() 15 } 16 }) 17</script> 18 19<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:
src/routes/+layout.server.ts1import type { LayoutServerLoad } from './$types'
2import { getServerSession } from '@supabase/auth-helpers-sveltekit'
3
4export const load: LayoutServerLoad = async (event) => {
5 return {
6 session: await getServerSession(event),
7 }
8}
In addition you can create a layout load function if you are using invalidate('supabase:auth')
:
src/routes/+layout.ts1import type { LayoutLoad } from './$types' 2import { getSupabase } from '@supabase/auth-helpers-sveltekit' 3 4export const load: LayoutLoad = async (event) => { 5 const { session } = await getSupabase(event) 6 return { session } 7}
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.
src/app.d.ts1/// <reference types="@sveltejs/kit" />
2
3// See https://kit.svelte.dev/docs/types#app
4// for information about these interfaces
5// and what to do when importing types
6declare namespace App {
7 interface Supabase {
8 Database: import('./DatabaseDefinitions').Database
9 SchemaName: 'public'
10 }
11
12 // interface Locals {}
13 interface PageData {
14 session: import('@supabase/supabase-js').Session | null
15 }
16 // interface Error {}
17 // interface Platform {}
18}
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.
src/routes/+page.svelte1<script> 2 import { page } from '$app/stores' 3</script> 4 5{#if !$page.data.session} 6<h1>I am not logged in</h1> 7{:else} 8<h1>Welcome {$page.data.session.user.email}</h1> 9<p>I am logged in!</p> 10{/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
:
1<script> 2 import { supabaseClient } from '$lib/db' 3 import { page } from '$app/stores' 4 5 let loadedData = [] 6 async function loadData() { 7 const { data } = await supabaseClient.from('test').select('*').limit(20) 8 loadedData = data 9 } 10 11 $: if ($page.data.session) { 12 loadData() 13 } 14</script> 15 16{#if $page.data.session} 17<p>client-side data fetching with RLS</p> 18<pre>{JSON.stringify(loadedData, null, 2)}</pre> 19{/if}
Server-side data fetching with RLS#
src/routes/profile/+page.svelte1<script> 2 /** @type {import('./$types').PageData} */ 3 export let data 4 $: ({ user, tableData } = data) 5</script> 6 7<div>Protected content for {user.email}</div> 8<pre>{JSON.stringify(tableData, null, 2)}</pre> 9<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
:
src/routes/profile/+page.ts1import type { PageLoad } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { redirect } from '@sveltejs/kit'
4
5export const load: PageLoad = async (event) => {
6 const { session, supabaseClient } = await getSupabase(event)
7 if (!session) {
8 throw redirect(303, '/')
9 }
10 const { data: tableData } = await supabaseClient.from('test').select('*')
11
12 return {
13 user: session.user,
14 tableData,
15 }
16}
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.ts1import type { RequestHandler } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { json, redirect } from '@sveltejs/kit'
4
5export const GET: RequestHandler = async (event) => {
6 const { session, supabaseClient } = await getSupabase(event)
7 if (!session) {
8 throw redirect(303, '/')
9 }
10 const { data } = await supabaseClient.from('test').select('*')
11
12 return json({ data })
13}
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
.
src/routes/posts/+page.server.ts1import type { Actions } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { error, invalid } from '@sveltejs/kit'
4
5export const actions: Actions = {
6 createPost: async (event) => {
7 const { request } = event
8 const { session, supabaseClient } = await getSupabase(event)
9 if (!session) {
10 // the user is not signed in
11 throw error(403, { message: 'Unauthorized' })
12 }
13 // we are save, let the user create the post
14 const formData = await request.formData()
15 const content = formData.get('content')
16
17 const { error: createPostError, data: newPost } = await supabaseClient
18 .from('posts')
19 .insert({ content })
20
21 if (createPostError) {
22 return invalid(500, {
23 supabaseErrorMessage: createPostError.message,
24 })
25 }
26 return {
27 newPost,
28 }
29 },
30}
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#
1import type { Actions } from './$types'
2import { invalid, redirect } from '@sveltejs/kit'
3import { getSupabase } from '@supabase/auth-helpers-sveltekit'
4
5export const actions: Actions = {
6 signin: async (event) => {
7 const { request, cookies, url } = event
8 const { session, supabaseClient } = await getSupabase(event)
9 const formData = await request.formData()
10
11 const email = formData.get('email') as string
12 const password = formData.get('password') as string
13
14 const { error } = await supabaseClient.auth.signInWithPassword({
15 email,
16 password,
17 })
18
19 if (error) {
20 if (error instanceof AuthApiError && error.status === 400) {
21 return invalid(400, {
22 error: 'Invalid credentials.',
23 values: {
24 email,
25 },
26 })
27 }
28 return invalid(500, {
29 error: 'Server error. Try again later.',
30 values: {
31 email,
32 },
33 })
34 }
35
36 throw redirect(303, '/dashboard')
37 },
38
39 signout: async (event) => {
40 const { supabaseClient } = await getSupabase(event)
41 await supabaseClient.auth.signOut()
42 throw redirect(303, '/')
43 },
44}
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.
src/hooks.server.ts1import type { RequestHandler } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { redirect, error } from '@sveltejs/kit'
4
5export const handle: Handle = async ({ event, resolve }) => {
6 // protect requests to all routes that start with /protected-routes
7 if (event.url.pathname.startsWith('/protected-routes')) {
8 const { session, supabaseClient } = await getSupabase(event)
9
10 if (!session) {
11 throw redirect(303, '/')
12 }
13 }
14
15 // protect POST requests to all routes that start with /protected-posts
16 if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
17 const { session, supabaseClient } = await getSupabase(event)
18
19 if (!session) {
20 throw error(303, '/')
21 }
22 }
23
24 return resolve(event)
25}
Migrate from 0.7.x to 0.8 #
Set up the Supabase client #
src/lib/db.ts1import { createClient } from '@supabase/supabase-js'
2import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
3import { dev } from '$app/environment'
4import { env } from '$env/dynamic/public'
5// or use the static env
6
7// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
8
9export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
10 persistSession: false,
11 autoRefreshToken: false,
12})
13
14setupSupabaseHelpers({
15 supabaseClient,
16 cookieOptions: {
17 secure: !dev,
18 },
19})
Initialize the client #
src/routes/+layout.svelte1<script lang="ts"> 2 // make sure the supabase instance is initialized on the client 3 import '$lib/db' 4 import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit' 5 import { page } from '$app/stores' 6 import { invalidateAll } from '$app/navigation' 7 8 // this sets up automatic token refreshing 9 startSupabaseSessionSync({ 10 page, 11 handleRefresh: () => invalidateAll(), 12 }) 13</script> 14 15<slot />
Set up hooks #
src/hooks.server.ts1// make sure the supabase instance is initialized on the server 2import '$lib/db' 3import { dev } from '$app/environment' 4import { auth } from '@supabase/auth-helpers-sveltekit/server' 5 6export const handle = auth()
Optional if using additional handle methods
src/hooks.server.ts1// make sure the supabase instance is initialized on the server 2import '$lib/db' 3import { dev } from '$app/environment' 4import { auth } from '@supabase/auth-helpers-sveltekit/server' 5import { sequence } from '@sveltejs/kit/hooks' 6 7export const handle = sequence(auth(), yourHandler)
Typings #
src/app.d.ts1/// <reference types="@sveltejs/kit" />
2
3// See https://kit.svelte.dev/docs/types#app
4// for information about these interfaces
5// and what to do when importing types
6declare namespace App {
7 interface Locals {
8 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
9 }
10
11 interface PageData {
12 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
13 }
14
15 // interface Error {}
16 // interface Platform {}
17}
withPageAuth #
src/routes/protected-route/+page.svelte1<script lang="ts"> 2 import type { PageData } from './$types' 3 4 export let data: PageData 5 $: ({ tableData, user } = data) 6</script> 7 8<div>Protected content for {user.email}</div> 9<p>server-side fetched data with RLS:</p> 10<pre>{JSON.stringify(tableData, null, 2)}</pre> 11<p>user:</p> 12<pre>{JSON.stringify(user, null, 2)}</pre>
src/routes/protected-route/+page.ts1import { withAuth } from '@supabase/auth-helpers-sveltekit'
2import { redirect } from '@sveltejs/kit'
3import type { PageLoad } from './$types'
4
5export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
6 if (!session.user) {
7 throw redirect(303, '/')
8 }
9
10 const { data: tableData } = await getSupabaseClient().from('test').select('*')
11 return { tableData, user: session.user }
12})
withApiAuth #
src/routes/api/protected-route/+server.ts1import type { RequestHandler } from './$types'
2import { withAuth } from '@supabase/auth-helpers-sveltekit'
3import { json, redirect } from '@sveltejs/kit'
4
5interface TestTable {
6 id: string
7 created_at: string
8}
9
10export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
11 if (!session.user) {
12 throw redirect(303, '/')
13 }
14
15 const { data } = await getSupabaseClient().from<TestTable>('test').select('*')
16
17 return json({ data })
18})
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.ts1import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit'; 2 3const { supabaseClient } = createSupabaseClient( 4 import.meta.env.VITE_SUPABASE_URL as string, 5 import.meta.env.VITE_SUPABASE_ANON_KEY as string 6); 7 8export { supabaseClient };
Initialize the client #
src/routes/__layout.svelte1<script> 2 import { session } from '$app/stores' 3 import { supabaseClient } from '$lib/db' 4 import { SupaAuthHelper } from '@supabase/auth-helpers-svelte' 5</script> 6 7<SupaAuthHelper {supabaseClient} {session}> 8 <slot /> 9</SupaAuthHelper>
Set up hooks #
src/hooks.ts1import { handleAuth } from '@supabase/auth-helpers-sveltekit' 2import type { GetSession, Handle } from '@sveltejs/kit' 3import { sequence } from '@sveltejs/kit/hooks' 4 5export const handle: Handle = sequence(...handleAuth()) 6 7export const getSession: GetSession = async (event) => { 8 const { user, accessToken, error } = event.locals 9 return { 10 user, 11 accessToken, 12 error, 13 } 14}
Typings #
src/app.d.ts1/// <reference types="@sveltejs/kit" />
2// See https://kit.svelte.dev/docs/types#app
3// for information about these interfaces
4declare namespace App {
5 interface UserSession {
6 user: import('@supabase/supabase-js').User
7 accessToken?: string
8 }
9
10 interface Locals extends UserSession {
11 error: import('@supabase/supabase-js').ApiError
12 }
13
14 interface Session extends UserSession {}
15
16 // interface Platform {}
17 // interface Stuff {}
18}
Check the user on the client#
src/routes/index.svelte1<script> 2 import { session } from '$app/stores' 3</script> 4 5{#if !$session.user} 6<h1>I am not logged in</h1> 7{:else} 8<h1>Welcome {$session.user.email}</h1> 9<p>I am logged in!</p> 10{/if}
withPageAuth#
src/routes/protected-route.svelte1<script lang="ts" context="module"> 2 import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit' 3 import type { Load } from './__types/protected-page' 4 5 export const load: Load = async ({ session }) => 6 withPageAuth( 7 { 8 redirectTo: '/', 9 user: session.user, 10 }, 11 async () => { 12 const { data } = await supabaseServerClient(session.accessToken).from('test').select('*') 13 return { props: { data, user: session.user } } 14 } 15 ) 16</script> 17 18<script> 19 export let data 20 export let user 21</script> 22 23<div>Protected content for {user.email}</div> 24<p>server-side fetched data with RLS:</p> 25<pre>{JSON.stringify(data, null, 2)}</pre> 26<p>user:</p> 27<pre>{JSON.stringify(user, null, 2)}</pre>
withApiAuth#
src/routes/api/protected-route.ts1import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit'
2import type { RequestHandler } from './__types/protected-route'
3
4interface TestTable {
5 id: string
6 created_at: string
7}
8
9interface GetOutput {
10 data: TestTable[]
11}
12
13export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
14 withApiAuth({ user: locals.user }, async () => {
15 // Run queries with RLS on the server
16 const { data } = await supabaseServerClient(request).from('test').select('*')
17
18 return {
19 status: 200,
20 body: { data },
21 }
22 })