Skip to main content

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

Edit your +layout.svelte file and set up the client-side.

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

The hooks.ts file is where the heavy lifting of this library happens:

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();

// use the sequence helper if you have additional Handle methods
import { sequence } from '@sveltejs/kit/hooks';

export const handle = sequence(auth(), yourHandler);

There are three handle methods available:

  • callback(): This creates a handler for /api/auth/callback. The client forwards the session details here every time onAuthStateChange fires on the client-side. This is needed to set up the cookies for your application so that SSR works seamlessly.
  • session(): This parses the session from the cookie and populate it in locals
  • auth(): a shorthand for sequence(callback(), session()) that uses both handlers

Send session to client

In order to make the session available to the UI (e.g., pages and layouts), pass the session in the root layout load function:

src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
return {
session: locals.session
};
};

Typings

In order to get the most out of TypeScript and IntelliSense, import the types into the app.d.ts type definition file that comes with your SvelteKit project.

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 {}
}

Signing out

This library has provided a method to delete the session cookie.

Create a server only page with an action to delete the cookie

src/routes/logout/+page.server.ts
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server';
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
async default({ cookies }) {
deleteSession(cookies);
throw redirect(303, '/');
}
};

Then make a POST request with a form to the server only page above.

<script lang="ts">
import { enhanceAndInvalidate } from '@supabase/auth-helpers-sveltekit';
</script>

<form action="/logout" method="post" use:enhanceAndInvalidate>
<button type="submit">Sign out</button>
</form>

Basic setup

You can now determine if a user is authenticated on the client-side by checking that the user object in $page.data.session is defined.

src/routes/+page.svelte
<script>
import { page } from '$app/stores';
</script>

{#if !$page.data.session.user}
<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 user is defined client-side in $page.data.session:

<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.user) {
loadData();
}
</script>

{#if $page.data.session.user}
<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
<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 withAuth helper to check if the user is authenticated. The helper extends the event with session and getSupabaseClient():

src/routes/profile/+page.ts
import type { PageLoad } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { redirect } from '@sveltejs/kit';

interface TestTable {
id: string;
created_at: string;
}

export const load: PageLoad = withAuth(async ({ getSupabaseClient, session }) => {
if (!session.user) {
throw redirect(303, '/');
}
const { data: tableData } = await getSupabaseClient()
.from<TestTable>('test')
.select('*');

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

Caution:

Always use the instance returned by getSupabaseClient() directly!

// Bad
const supabaseClient = getSupabaseClient();

await supabaseClient.from('table1').select();
await supabaseClient.from('table2').select();

// Good
await getSupabaseClient().from('table1').select();
await getSupabaseClient().from('table2').select();

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
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 });
);

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.ts
import type { Actions } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { error, invalid } from '@sveltejs/kit';

export const actions: Actions = {
createPost: withAuth(async ({ session, getSupabaseClient, request }) => {
if (!session.user) {
// 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 getSupabaseClient()
.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

Use saveSession to save the session cookies:

import type { Actions } from './$types';
import { supabaseClient } from '$lib/db';
import { invalid, redirect } from '@sveltejs/kit';
import { saveSession } from '@supabase/auth-helpers-sveltekit/server';

export const actions: Actions = {
async signin({ request, cookies, url }) {
const formData = await request.formData();

const email = formData.get('email') as string;
const password = formData.get('password') as string;

const { data, error } = await supabaseClient.auth.api.signInWithEmail(
email,
password,
{
redirectTo: `${url.origin}/logging-in`
}
);

if (error || !data) {
if (error?.status === 400) {
return invalid(400, {
error: 'Invalid credentials',
values: {
email
}
});
}
return invalid(500, {
error: 'Server error. Try again later.',
values: {
email
}
});
}

saveSession(cookies, data);
throw redirect(303, '/dashboard');
}
};

Use deleteSession to delete the session cookies:

import type { Actions } from './$types';
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server';
import { redirect } from '@sveltejs/kit';

export const actions: Actions = {
async logout({ cookies }) {
deleteSession(cookies);
throw redirect(303, '/');
}
};

Custom session namespace

If you want to use something other than locals.session and $page.data.session, update the types and create three helper functions:

src/app.d.ts
declare namespace App {
interface Locals {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
interface PageData {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
}
src/hooks.server.ts
setupSupabaseServer({
supabaseClient,
cookieOptions: {
secure: !dev
},
// --- change location within locals ---
getSessionFromLocals: (locals) => locals.mySupabaseSession,
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session)
});
src/lib/db.ts
setupSupabaseClient({
supabaseClient,
// --- change location within pageData ---
getSessionFromPageData: (data) => data.mySupabaseSession
});

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 }
};
});