Securing Your SvelteKit App with Supabase Authentication

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.

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

// 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!