EDIT: This tutorial was originaly released for @supabase/supabase-auth-helpers package and later rewritten for @supabase/ssr package beta version. Supabase is still making some significant changes in SSR package as heading towards 1.0 release. You can follow updates here. I am trying to keep it as uptodate as possible. This tutorial is based on @supabase/ssr: 0.4.0
, @supabase/supabase-js: 2.44.2
and @sveltejs/kit: 2.0.0
.
N.B.: There is an infamous warnning log that Supabase added. This tutorial avodis it as much as possible, but even standard internal Supabase methods such as auth.updateUser
fire this warning log. Let's hope Supabase will solve this soon.
Supabase @supabase/ssr package
Supabase recently introduced a @supabase/ssr package instead of their @supabase/supabase-auth-helpers package. Supabase generally recommends using the new @supabase/ssr package which takes the core concepts of the Auth Helpers package and makes them available to any server framework. The Supabase Auth Helpers will be probably deprecated later on as this package is not maintained anymore.
@supabase/ssr package is ment to be used easily with any framework wich includes backend such as Next.js, SvelteKit, Astro, Remix or Express. This was the main idea of Supabase to have just one general easier to maintain package which was not the case for Auth Helpers.
This tutorial walks you through the process how to use @supabase/ssr package with Sveltekit. The implementation is very easy, smooth and rather straightforward.
Create SvelteKit Project
Create the SvelteKit app and name it for example "my-sk-app-with-sb-ssr-auth".
npm create svelte@latest my-sk-app-with-sb-ssr-auth
cd my-sk-app-with-sb-ssr-auth
npm install
Now install relevant Supabase packages:
npm install @supabase/ssr @supabase/supabase-js
Create Supabase project
If you do not have your Supabase project create the new one. Just follow the instructions on https://supabase.com/ and start the new project. From your Project Settings dashboard in section API details copy SUPABASE_URL and SUPABASE_ANON_KEY keys which are to be used in a front end of your application.
Public Variables
Create a .env.local file in your SvelteKit project root directory. Use your SUPABASE_URL and SUPABASE_ANON_KEY keys whcih you have just copied from your Supabase project's dashboard.
# .env.local
PUBLIC_SUPABASE_URL=your_supabase_project_url
PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
Creating Supabase createServerClient in Hooks
In your SvelteKit project root directory create a hooks.server.js file. In this file we are settig up Supabase server client using imported env keys. Creating a Supabase client with the ssr package automatically configures it to use Cookies.
You might be currious why we are repopulating the user in the session received via supabase.auth.getSession() wtih the user from supabase.auth.getUser(). This is to avoid infamous warnings that Supabase logs if you are using the user object in your application as returned from supabase.auth.getSession(). When Supabase warning logs will be solved there will be no need for this session.user hack.
You may even silence the warning as you can see in if 'suppressGetSessionWarning
block of code here udner' but this is rather a heck.
There is also a solution which validates the session using JWT secrete and jose library. For more details you may go to this repo.
// hooks.server.js
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createServerClient } from '@supabase/ssr'
export const handle = async ({ event, resolve }) => {
event.locals.supabaseServerClient = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll() {
return event.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
event.cookies.set(name, value, { ...options,path: '/' })
)
},
},
})
// if you want to silence the warnings https://github.com/supabase/auth-js/issues/873
if ('suppressGetSessionWarning' in event.locals.supabaseServerClient.auth) {
// @ts-expect-error - suppressGetSessionWarning is not part of the official API
event.locals.supabaseServerClient.auth.suppressGetSessionWarning = true;
} else {
console.warn(
'SupabaseAuthClient#suppressGetSessionWarning was removed. See https://github.com/supabase/auth-js/issues/888.',
);
}
const getSessionAndUser = async () => {
const { data: { session } } = await event.locals.supabaseServerClient.auth.getSession()
if (!session) {
return {
session: null,
user: null
}
}
const { data: { user }, error } = await event.locals.supabaseServerClient.auth.getUser()
if (error) {
// JWT validation has failed
return {
session: null,
user: null
}
}
delete session.user
const sessionWithUserFromUser = { ...session, user: {...user} }
return { session: sessionWithUserFromUser, user }
}
const { session, user } = await getSessionAndUser()
event.locals.session = session
event.locals.user = user
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range' || name === 'x-supabase-api-version'
},
})
}
Returning Session from Root Server Layout
Create +layout.server.js file in the routes directory. This file will just pass the session with a respective user.
// src/routes/+layout.server.js
export const load = async (event) => {
// clearing the cookie from a browser if the user logs out or was deleted from the database
if (event.locals.session == null) {
event.cookies.delete(event.locals.supabaseServerClient.storageKey, { path: '/' });
}
return {
session: event.locals.session,
user: event.locals.user
};
};
Creating Supabase createBrowserClient in Root Layout Load
Now we will create +layout.js file in a routes directory. In this file we will set up Supabase browser client. Page components can get access to the Supabase client from the data object due to this load function.
Note that you may not need the Supabase browser client if you do all the stuff server side.
The very important part of the code is to use depends('supabase:auth')
. 'supabase:auth' is just an identifier, you can name it however you want. If later on we will need to reload this load function (particularly when user's auth state changes) we can do so using invalidate function after such a change with this identifier 'supabase:auth' as an argument (i.e. invalidate('supabase:auth')
). You will see us using this feature in src/routes/+layout.svelte later on.
// src/routes/+layout.js
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
export const load = async ({ fetch, data, depends }) => {
depends('supabase:auth')
const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
})
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
cookies: {
getAll() {
return data.cookies
},
},
})
const session = isBrowser()
? (await supabase.auth.getSession()).data.session
: data.session
return {
supabase,
session,
user: data.user
}
}
Routes Layout Page
It seems as a good practice to use SvelteKit layout page as a login/logout header navigation. I have added just a little bit of css to move loginn/logout nav to the top right corner.
The important stuff is to add onMout and listen to the onAuthStateChange of Supabase (mainly to listen to the events when user logs in or out).
If such changes happen we will invalidate all relevant load functions.
One way is to put into such load function depends function. In this depends function we may specify on which invalidation it dependes. So in our case it depends on "supabase:auth" (i.e. depends('supabase:auth')
. We have already done so in load function in a root layout.js file as you can see above. Then in svelte file where we need invalidation in matter to run we can call named invalidation (i.e. invalidate('supabase:auth')
). You should carefully decide what is linked to what. Root +layout.js and +layout.svelte are probably quite top level enough.
The other more general and bulletproof attitude is to use invalidateAll()
. You put it in svelte file when some change may happen and thus reload all load functions of your application. Without bothering to guess to which load function you should add that it depends on such a change.
As auth is quite sensitive I would rather recommend to call more general invalidateAll()
function instead, just to be sure all load functions are rerun.
In example code bellow I am evem using both invalidations.
In any case invalidation will sync server Supabase client and the browser Supabase client as the load function(s) will rerun. So you will update/sync the session state as well all browser tabs where you may run the app.
The layout has the logout form for logged in user as well and calls the repsective logout route/action. The relevant endpoint in /logout route is descibed in a secton Route with Logout Logic.
// src/routes/+layout.svelte
<script>
import { enhance } from '$app/forms';
import { invalidate, invalidateAll, goto } from '$app/navigation';
import { onMount } from 'svelte';
export let data;
$: ({ supabase } = data);
onMount(async () => {
const {
data: { subscription }
} = supabase.auth.onAuthStateChange((event, _session) => {
// If you want to fain grain which routes should rerun their load function
// when onAuthStateChange changges
// use invalidate('supabase:auth')
// which is linked to +layout.js depends('supabase:auth').
// This should mainly concern all routes
//that should be accesible only for logged in user.
// Otherwise use invalidateAll()
// which will rerun every load function of you app.
invalidate('supabase:auth');
invalidateAll();
});
return () => subscription.unsubscribe();
});
const submitLogout = async ({ cancel }) => {
const { error } = await data.supabase.auth.signOut();
if (error) {
console.log(error);
}
cancel();
await goto('/');
};
</script>
<a href="/">Home</a>
<a href="/subscription">Subscriptions</a>
<span id="auth_header">
{#if !data.session}
<a href="/login">login</a> / <a href="/register">signup</a>
{:else}
<a href="/user_profile">User profile</a>
<form action="/logout?/logout" method="POST" use:enhance={submitLogout}>
<button type="submit">Logout</button>
</form>
{/if}
</span>
<slot />
<style>
#auth_header {
float: right;
}
form {
display: inline;
}
</style>
Email Auth with PKCE flow for SSR
We will use email authentication. In order to use the updated email links we will need to setup an endpoint for verifying the token_hash along with the type to exchange token_hash for the user's session which is set as a cookie for future requests made to Supabase. This endpoint will be used mainly for auth emails connfirmations.
Create a new file at src/routes/auth/confirm/+server.js and populate with the following:
// src/routes/auth/confirm/+server.js
import { redirect } from '@sveltejs/kit';
export const GET = async (event) => {
const {
url,
locals: { supabaseServerClient }
} = event;
const token_hash = url.searchParams.get('token_hash');
const type = url.searchParams.get('type');
const next = url.searchParams.get('next') ?? '/';
if (token_hash && type) {
const { error } = await supabaseServerClient.auth.verifyOtp({ token_hash, type });
if (!error) {
redirect(303, `/${next.slice(1)}`);
}
}
// return the user to an error page with some instructions
redirect(303, '/auth/auth-code-error');
};
And for convenience we may provide the herebaove mentioned auth-code-error error page.
// src/routes/auth/auth-code-error/+page.svelte
There was some logging error.
Simple Home Page
Create simple home lannding page.
// src/routes/+page.svelte
<h1> Welcome to this website ...</h1>
Register Route
Finally we will do some logging logic. First of all we will create register page as well as relevant page server file. Notice that we are also conditionaly dsiplaying error message in case the form submission was invalid and errored with 4xx client errors HTTP response status.
// src/routes/register/+page.svelte
<script>
import { enhance } from '$app/forms';
export let form;
</script>
<h2>Sign Up</h2>
<form action="?/register" method="POST" use:enhance>
<label for="email">email</label>
<input name="email" type="email" value={form?.email ?? ''} required />
<label for="password">password</label>
<input name="password" required />
<button type="submit">Sign up</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
And the corresponding +page.server.js file logic is as follows. In a nutshell here we are just calling supabase.auth.signUp()
providing it with the email and password, handling possible errors and redirecting user eventually to check email page.
// src/routes/register/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
import { AuthApiError } from '@supabase/supabase-js'
export const actions = {
register: async (event) => {
const { request, locals } = event
const formData = await request.formData()
const email = formData.get('email')
const password = formData.get('password')
const { data, error: err } = await locals.supabaseServerClient.auth.signUp({
email: email,
password: password
})
if (err) {
if (err instanceof AuthApiError && err.status >= 400 && err.status < 500) {
return fail(400, {
error: "invalidCredentials", email: email, invalid: true, message: err.message
})
}
return fail(500, {
error: "Server error. Please try again later.",
})
}
// signup for existing user returns an obfuscated/fake user object without identities https://supabase.com/docs/reference/javascript/auth-signup
if (!err && !!data.user && !data.user.identities.length ) {
return fail(409, {
error: "User already exists", email: email, invalid: true, message: "User already exists"
})
}
redirect(303, "/check_email");
}
}
export const load = async ({ locals }) => {
// if the user is already logged in redirect to the home page
if (locals.session) {
redirect(303, '/');
}
}
You also need to update Supabase auth email templates.
Go to your Supabase project dashboard website and in Authentication secition update Confirm signup email template like this.
<h2>Confirm your signup</h2>
<p>Follow this link to confirm your user:</p>
<p>
<a href="http://localhost:5173/auth/confirm?token_hash={{ .TokenHash }}&type=email"
>Confirm your email</a>
</p>
N.B. Do not forget to change "http://localhost:5173" to your app website address in all Supabase email templates when you host your app eventually.
Check Email Route
Create check_email route with simple +page.svelte file.
// src/routes/check_email/+page.svelte
<p>Check your email to confirm.</p>
Route with Login Logic
Create login route which will enable user to login.
// src/routes/login/+page.svelte
<script>
import { enhance } from '$app/forms';
export let form;
</script>
<h2>Log in</h2>
<form action="?/login" method="POST" use:enhance>
<label for="email">email</label>
<input name="email" type="email" value={form?.email ?? ''} required />
<label for="password">password</label>
<input name="password" required />
<button type="submit">Login</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
<p>Forgot your password? <a href="/reset_password">Reset password</a></p>
The respective +page.server.js file contains action for login. Simply speaking in case of login we are just calling supabase.auth.signInWithPassword()
providing it with email and password, handling possible errors and redirecting user eventually.
// src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import { AuthApiError } from '@supabase/supabase-js';
export const actions = {
login: async (event) => {
const { request, url, locals } = event;
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
const { data, error: err } = await locals.supabaseServerClient.auth.signInWithPassword({
email: email,
password: password
});
if (err) {
if (err instanceof AuthApiError && err.status === 400) {
return fail(400, {
error: 'Invalid credentials',
email: email,
invalid: true,
message: err.message
});
}
return fail(500, {
message: 'Server error. Try again later.'
});
}
redirect(307, '/');
},
}
export const load = async ({ locals }) => {
// if there is a user's session redirect back to the home page
if (locals.session) {
redirect(303, '/');
}
}
Route with Logout Logic
In case of logout we are calling supabase.auth.signOut()
, which also automatically deletes user cookie, and redirects to home page. There is just the +page.server.js file, this is why I am adding uncoditional redirect and no +page.svelte file. The client form for logout is already in +layout.svelte as mentioned hereabove.
// src/routes/logout/+page.server.js
import { redirect } from '@sveltejs/kit';
export const actions = {
logout: async ({ locals }) => {
await locals.supabaseServerClient.auth.signOut()
redirect(303, '/');
}
}
// no one should visit this page
export async function load() {
redirect(303, '/');
}
Reset Password
Make route for password reset called reset_password, Once again one +page.svelte file and one +page.server.js file.
// src/routes/reset_password/+page.svelte
<script>
import { enhance } from '$app/forms';
export let form
</script>
<h2>Where should we send you a link for password reset?</h2>
<form action="?/reset_password" method="POST" use:enhance>
<label for="email">email</label>
<input type="email" name="email" placeholder="name@domain.com" required />
<button type="submit">Get password</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
In a nutshell in reset_password/+page.server.js we are just calling supabase.auth.resetPasswordForEmail()
providing it with email and route of a password update page, handling possible errors and redirecting user eventually.
// src/routes/reset_password/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
import { AuthApiError } from "@supabase/supabase-js"
export const actions = {
reset_password: async ({ request, locals }) => {
const formData = await request.formData()
const email = formData.get('email')
const { data, error: err } = await locals.supabaseServerClient.auth.resetPasswordForEmail(
email,
{redirectTo: '/update_password'}
)
if (err) {
if (err instanceof AuthApiError && err.status === 400) {
return fail(400, {
error: "invalidCredentials", email: email, invalid: true, message: err.message
})
}
return fail(500, {
error: "Server error. Please try again later.",
})
}
redirect(303, "/check_email");
},
}
export const load = async ({ locals }) => {
// if the user is already logged in redirect to the home page
if (locals.session) {
redirect(303, '/');
}
}
The Supabase email template for Reset password looks like this. The link will send the user to /update_password of our application.
<h2>Reset Password</h2>
<p>Follow this link to reset the password for your user:</p>
<p>
<a
href="http://localhost:5173/auth/confirm?token_hash={{ .TokenHash }}&type=recovery&next=/update_password"
>Reset Password</a
>
</p>
Update Password Route
As already mentioned resetting password needs the route update_password where the user may insert her/his new password. Lets create this update_password route.
// src/routes/update_password/+page.svelte
<script>
import { enhance } from '$app/forms';
export let form
</script>
<h2>Change your password</h2>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
<form action="?/update_password" method="POST" use:enhance>
<label for="new_password"> New password </label>
<input name="new_password" required/>
<label for="password_confirm">Confirm new password</label>
<input name="password_confirm" required/>
<button>Update password</button>
</form>
Simply speaking here we are just calling supabase.auth.updateUser()
providing it with new password, handling possible errors and redirecting user eventually to her/his profile page.
// src/routes/update_password/+page.server.js
import { AuthApiError } from "@supabase/supabase-js"
import { fail, redirect } from "@sveltejs/kit"
export const actions = {
update_password: async ({ request, locals }) => {
const formData = await request.formData()
const password = formData.get('new_password')
const { data, error: err } = await locals.supabaseServerClient.auth.updateUser({
password
})
if (err) {
if (err instanceof AuthApiError && err.status >= 400 && err.status < 500) {
return fail(400, {
error: "invalidCredentials", invalid: true, message: err.message
})
}
return fail(500, {
error: "Server error. Please try again later.",
})
}
redirect(303, "/user_profile");
},
}
export const load = async ({ locals }) => {
// if there is no user's session redirect back to the home page
if (!locals.session) {
redirect(303, '/');
}
}
Update Email Route
User may wish to update her/his emial so here is the update_email route to do this. Remeber the confirmaton from both emails (the old one as well as the new one) has to be provided.
// src/routes/update_email/+page.svelte
<script>
import { enhance } from '$app/forms';
export let form;
</script>
<h2>Change your email</h2>
<form action="?/update_email" method="POST" use:enhance>
<label for="email"> new email </label>
<input type="email" name="email" required />
<button>Change email</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
In a nutshell here we are just calling supabase.auth.updateUser()
this time providing it with the new email, handling possible errors and redirecting user to her/his profile page eventually.
// src/routes/update_email/+page.server.js
import { AuthApiError } from "@supabase/supabase-js"
import { fail, redirect } from "@sveltejs/kit"
export const actions = {
update_email: async ({ request, locals }) => {
const formData = await request.formData()
const email = formData.get('email')
const { data, error: err } = await locals.supabaseServerClient.auth.updateUser({
email
})
if (err) {
if (err instanceof AuthApiError && err.status >= 400 && err.status < 500) {
return fail(400, {
error: "invalidCredentials", invalid: true, message: err.message
})
}
return fail(500, {
error: "Server error. Please try again later.",
})
}
redirect(303, "/check_email");
},
}
export const load = async ({ locals }) => {
// if there is no user's session redirect back to the home page
if (!locals.session) {
redirect(303, '/');
}
}
And the Supabase email template for Change Email Address looks like this.
<h2>Confirm Change of Email</h2>
<p>Follow this link to confirm the update of your email from {{ .Email }} to {{ .NewEmail }}:</p>
<p>
<a href="http://localhost:5173/auth/confirm?token_hash={{ .TokenHash }}&type=email_change">
Change Email
</a>
</p>
User Profile Route
We are still missing user profile route where user can manage the account. Let create user_profile route with respective files.
// src/routes/user_profile/+page.svelte
<script>
export let data;
</script>
<h2>User profile</h2>
<p>{data.session.user.email}</p>
<p><a href="/update_email">Change your email</a></p>
<p><a href="/update_password">Change password</a></p>
<p><a href="/delete_user">Delete my account</a></p>
The page should be accesible only to logged in user I guess.
// src/routes/user_profile/+page.server.js
import { redirect } from "@sveltejs/kit"
export const load = async ({ locals }) => {
// redirect if there is no user's session
if (!locals.session) {
redirect(303, '/');
}
return {
session: locals.session,
user: locals.user
}
}
Delete User Account Route
The opinions may differ wheter we should enable user to delete her/his account. But as this may seem tricky in Supabase here is the way. Lets make delete_user route.
// src/routes/delete_user/+page.svelte
<script>
import { enhance } from '$app/forms';
export let form
</script>
<h2>Delete your user account</h2>
<form action="?/delete_user" method="POST" use:enhance>
<button type="submit">Delete my user account</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
Supbase uses special auth client created with secrete service role key to delete user. But the client created with the service role key has really high superuser/admin priviledges. If you do not want to deal with this mighty key here is a trick.
In Supabase dashboard go to SQL Editor.
In the SQL Editor paste in and run this function.
CREATE or replace function delete_user()
returns void
LANGUAGE SQL SECURITY DEFINER
AS $$
--delete from public.profiles where id = auth.uid();
delete from auth.users where id = auth.uid();
$$;
Now you can use this Supabase database delete_user()
function from server using supabase.rpc method with the name of this delete_user database function as an argument like this.
// src/routes/delete_user/+page.server.js
import { PRIVATE_RPC_DELETE_USER } from '$env/static/private'
import { redirect } from "@sveltejs/kit"
export const actions = {
delete_user: async ({ locals, request, cookies }) => {
const storageKey = locals.supabaseServerClient.storageKey
await locals.supabaseServerClient.rpc('delete_user');
cookies.delete(storageKey, { path: '/' });
redirect(303, "/");
}
}
export const load = async ({ locals }) => {
// if there is no user's session redirect back to the home page
if (!locals.session) {
redirect(303, '/');
}
}
It is also important to delete user cookie. The name of the cookie can be found in supabase.storageKey
. Because of this cookie deletion application kicks user out from all pages where session is requested.
Project Structure Overview
Here goes project structure of all the +page.svelte and +page.server.js files tree printscreen. (There are also routes for Stripe subscription so don't get confused)
Thank You for Reading
So this is it. Feel free to comment if something does not work for you. And if you find this tutorial useful at least a little please give it a like.
You may move protected routes logic to one place (probably into hooks) for example not to repeate it in relevant +page.server.js files.
I hope to post something soon, been busy this year with a SvelteKit project but now it is nearly done so more time for blogging.
EDIT December 11, 2023:
Added usecase for signup/registration of already exising user.
Added usecase to clear cookie if the user was deleted in the database.
EDIT December 12, 2023:
As some of you have noticed there was addUserprofileToUser() helper function. I deleted this part of the code to avoid any confusion. This addUserprofileToUser() utility function is used to enrich user data in her/his session. The reason is that Supabase does not allow you to add any data to Authetication table. For that you have to create your new table (for example "user_profile" table with its primary key (id) referencing Authetication table id).
My addUserprofileToUser() function looks like this:
export default async function addUserprofileToUser (session, supabase, user) {
if (session) {
let { data, error } = await supabase
.from('user_profile')
.select("*")
.eq('id', user.id)
.single()
user.user_profile = data
}
}
The hooks.server.js with addUserprofileToUser will look like this.
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createServerClient } from '@supabase/ssr'
import addUserprofileToUser from './utils/addUserprofileToUser'
...
delete session.user
await addUserprofileToUser(session, event.locals.supabaseServerClient, user)
const sessionWithUserFromUser = { ...session, user: {...user} }
return { session: sessionWithUserFromUser, user }
...
The addUserprofileToUser function simply gets the data from the user_profile table for the particular user and enriches her/his session. I call this function await addUserprofileToUser(session,event.locals.supabase)
in hooks.server.js.
In the profile table there is for example Stripe id of the user, her/his subpscription plan etc. But this Stripe SaaS features would need the whole new tutorial :-) Maybe next time.
EDIT February 7, 2024:
Update to help migrating to SvelteKit v2.
EDIT February 11, 2024:
I have updated the tutorial to fully implement invalidate('supabase:auth');
. Added separate logout route as well. So client part of the applicatoin refreshes relevant load functions correctly when needed and all is in sync including app opened in more browser tabs.
EDIT July 9, 2024:
Update because of Supabase SSR package update.
EDIT October 11, 2024:
Supabase introduces asymmetric JWTs and API keys breaking changes. You may learn more details in my post Supabase Introduces Auth Asymmetric JWTs and API Keys Breaking Changes.