Home

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 },
    }
  })
Need some help?

Not to worry, our specialist engineers are here to help. Submit a support ticket through the Dashboard.