Illustration Credits: Bianca Van Dijk
Setting up Supabase auth in my SvelteKit project wasn't as smooth as I hoped. After searching forums and docs, I found that many others were facing the same struggle. So, I decided to create this guide to help out! Let's walk through integrating Supabase auth into your SvelteKit app step-by-step, using TypeScript for added clarity.
https://kit.svelte.dev/docs/creating-a-project
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
To create a Supabase project, you typically follow these steps:
Go to the Supabase website where you can sign up for a free account.
If you already have an account, log in.
Once logged in, go to your dashboard and click on the "New project" button. You will be prompted to enter an organization name, a project name and a region for your project.
After the database has been created, you can find your project details, including the API URL, API Key, and other settings here:
https://supabase.com/dashboard/project/[your-project-id]/settings/api
While there will be some differences we’ll follow along with the supabase docs.
https://supabase.com/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr?framework=sveltekit
Install the required dependencies
npm install @supabase/ssr @supabase/supabase-js
Create a .env.local file in your project root directory. You can get your SUPABASE_URL and SUPABASE_ANON_KEY from inside your Supabase project's dashboard.
PUBLIC_SUPABASE_URL=your_supabase_project_url
PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
Creating a Supabase client with the ssr package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks.
Before configuring the necessary files, let's add some types to app.d.ts
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
import { SupabaseClient, Session } from '@supabase/supabase-js';
import type { Database } from './DatabaseDefinitions';
declare global {
namespace App {
// interface Error {}
interface Locals {
supabase: SupabaseClient<Database>;
getSession(): Promise<Session | null>;
}
interface PageData {
session: Session | null;
}
// interface PageState {}
// interface Platform {}
}
}
export { type Database };
The handle function runs every time the SvelteKit server receives a request. To add custom data to the request, which is passed to handlers in +server.js and server load functions, populate the event.locals object. Here a server side supabase client & a session property are added to the event.locals object
//src/hook.server.ts
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import { createServerClient } from '@supabase/ssr';
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
/**
* Creates a server side Supabase client that will be available
* throughout the application via the locals property
**/
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
get: (key) => event.cookies.get(key),
/**
* Note: You have to add the `path` variable to the
* set and remove method due to sveltekit's cookie API
* requiring this to be set, setting the path to an empty string
* will replicate previous/standard behaviour
* (https://kit.svelte.dev/docs/types#public-types-cookies)
*/
set: (key, value, options) => {
event.cookies.set(key, value, { ...options, path: '/' });
},
remove: (key, options) => {
event.cookies.delete(key, { ...options, path: '/' });
}
}
});
/**
* 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';
}
});
};
// src/routes/+layout.ts
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
import type { SupabaseClient } from '@supabase/supabase-js';
import { browser } from '$app/environment';
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ fetch, depends, data }) => {
depends('supabase:auth');
let supabase: SupabaseClient;
# Creates a browser side Supabase client
if (browser) {
supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
},
cookies: {
get(key) {
if (!isBrowser()) {
return JSON.stringify(data.session);
}
const cookie = parse(document.cookie);
return cookie[key];
}
}
});
const {
data: { session }
} = await supabase.auth.getSession();
return { supabase, session };
}
return { supabase: null, session: null };
};
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
return {
session: await getSession()
};
};
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { invalidate } from '$app/navigation';
import { onDestroy, onMount } from 'svelte';
import type { LayoutData } from './$types';
import type { Subscription } from '@supabase/supabase-js';
export let data: LayoutData;
let subscription: Subscription;
// Both the session & the supabase client are now available on the browser side, for all page components
$: ({ supabase, session } = data);
onMount(async () => {
if (supabase) {
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth');
}
});
subscription = data.subscription;
}
});
onDestroy(() => {
if (typeof subscription !== 'undefined') {
subscription.unsubscribe();
}
});
</script>
<slot />
// ../server.ts
import { redirect } from '@sveltejs/kit';
// The server side supabase client is available throughout the application
export const GET = async ({ locals:{supabase})=>{
}
import type { Actions } from './$types'
// Similarly, your server side supabase client is available in your SvelteKit actions
export const actions: Actions = {
default: async (event) => {
const { request, url, locals: { supabase } } = event
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
...
}
}
The Email provider should be enabled by default, but you can check the list of providers in your Supabase dashboard: https://supabase.com/dashboard/project/[your-project-id]/auth/providers
Redirect the users to a page informing them that they will receive an email with a link
async function signUpNewUser() {
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
options: {
emailRedirectTo: 'https://example.com/welcome',
},
})
}
I’ve set it at src/routes/api/auth/callback/+server.ts but the choice is yours.
import { redirect } from '@sveltejs/kit';
import type { EmailOtpType } from '@supabase/supabase-js';
export const GET = async ({ url, locals: { supabase } }) => {
const token_hash = url.searchParams.get('token_hash') as string;
const type: EmailOtpType = url.searchParams.get('type') as EmailOtpType;
const next = url.searchParams.get('next') ?? '/';
if (token_hash && type) {
const { error } = await supabase.auth.verifyOtp({ token_hash, type });
if (!error) {
throw redirect(303, `/${next.slice(1)}`);
}
}
// return the user to an error page with some instructions
throw redirect(303, '/auth-code-error');
};
Then inform Supabase of your API endpoint by creating an Email template: https://supabase.com/dashboard/project/[your-project-id]/auth/templates
<h2>Confirm your signup</h2>
<p>Follow this link to confirm your user:</p>
<p>
<a href="{{ .SiteURL }}/api/auth/callback?token_hash={{ .TokenHash }}&type=email"
>Confirm your email</a
>
</p>
Practically, a user signs up, receives an email, then clicks the confirmation link. The authentication token is checked at the api endpoint. If successful a session is created and the user logged in.
Check the other templates for a similar flow
async function signInWithEmail() {
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password',
})
}
async function signOut() {
const { error } = await supabase.auth.signOut()
}
With the session available throughout the application, we can check the authentication status server side.
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { getSession } }) => {
const session = await getSession();
// The user is not logged in!
if (!session) {
redirect(303, '/');
}
return {};
}) satisfies PageServerLoad;
Please check my repo where all the above is implemented:
https://github.com/psegarel/supabase_auth-sveltekit4
While the Supabase documentation offers a solid foundation, occasional ambiguities can lead to roadblocks, as many of us have experienced. This guide provides a step-by-step approach tailored to the SvelteKit and TypeScript environment, but remember, the official documentation remains an invaluable resource. For deeper dives, edge cases, and future updates, consult the Supabase docs!