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.
Setting Up SvelteKit:
https://kit.svelte.dev/docs/creating-a-project
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
Setting Up Supabase
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.
Create a New Project:
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.
Project Setup:
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
Creating a Supabase client for SSR
Install the required dependencies
npm install @supabase/ssr @supabase/supabase-js
Set environment variables
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.
app.d.ts
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 };
hooks.server.ts
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 thi "w/ait 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
// 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
// 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
<!-- 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 />
Any server.ts file
// ../server.ts
import { redirect } from '@sveltejs/kit';
// The server side supabase client is available throughout the application
export const GET = async ({ locals:{supabase})=>{
}
Any actions...
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
...
}
}
Implementing Email & Password Authentication
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
Sign up the user
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',
},
})
}
Confirmation API endpoint
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
Sign in the user
async function signInWithEmail() {
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password',
})
}
Sign out the user
async function signOut() {
const { error } = await supabase.auth.signOut()
}
Protecting Routes
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;
Conclusion
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!