EDIT: Having discussion with Supabase concerning best parctices. My actual recomended way can be followd here The SvelteKit SSR Documentation Examples Enhancements #742
EDIT 2: Supabase recently updated their official documentation accordingly.
Supabase recently introduced a @supabase/ssr package instead of their @supabase/supabase-auth-helpers packager. 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.
@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.
SvelteKit 2 vs SvelteKit 1
This tutorial is rewritten now for SvelteKit 2.x. If you are still using SvelteKit 1 you will need to add throw
before redirects and before errors. In SvelteKit 2 redirect and error are no longer thrown by you.
From my own experience using npx svelte-migrate@latest sveltekit-2
for migration to SvelteKit 2 clears all this throw
nicely and automaticaly for you.
One significant change is in hooks.server.js, src/routes/+layout.server.js and src/routes/delete_user/+page.server.js files. I show examples for both versions of SvelteKit. The case here is that path is required when setting or deleting cookies.
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. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks. It just works! API routes, server layout load and form actions can now access the supabase client from the event object due to this hook.
EDIT: Supabase has added important warning concerning SvelteKit: Beware when accessing the session object on the server, because it is not revalidated on every request from the client. That means the sender can tamper with unencoded data in the session object. If you need to verify the integrity of user data for server logic, call auth.getUser instead, which will query the Supabase Auth server for trusted user data.
This risk can be removed if you call getUser() as you can see hereunder.
// SvelteKit v2
// 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.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 '/'
* 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 () => {
/**
* getUser will guarantee that the stored session is valid,
* and calling getSession immediately after
* will leave no room for anyone to modify the stored session.
*/
const { data: getUserData, error: err } = await event.locals.supabase.auth.getUser()
let {
data: { session },
} = await event.locals.supabase.auth.getSession()
// solving the case if the user was deleted from the database but the browser still has a cookie/loggedin user
// +lauout.server.js will delete the cookie if the session is null
if (getUserData.user == null) {
session = null
}
return session
}
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
})
}
If you are still using SvelteKit v1 path was not required when setting cookies.
// SvelteKit v1
// 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.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
get: (key) => event.cookies.get(key),
set: (key, value, options) => {
event.cookies.set(key, value, options)
},
remove: (key, options) => {
event.cookies.delete(key, options)
},
},
})
/**
* 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 () => {
/**
* getUser will guarantee that the stored session is valid,
* and calling getSession immediately after
* will leave no room for anyone to modify the stored session.
*/
const { data: getUserData, error: err } = await event.locals.supabase.auth.getUser()
let {
data: { session },
} = await event.locals.supabase.auth.getSession()
// solving the case if the user was deleted from the database but the browser still has a cookie/loggedin user
// +lauout.server.js will delete the cookie if the session is null
if (getUserData.user == null) {
session = null
}
return session
}
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
})
}
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) => {
let session = await event.locals.getSession();
return {
session
};
};
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 { combineChunks, createBrowserClient, isBrowser, parse } from '@supabase/ssr'
export const load = async ({ fetch, data, depends }) => {
depends('supabase:auth')
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
cookies: {
get(key) {
if (!isBrowser()) {
return JSON.stringify(data.session)
}
const cookie = combineChunks(key, (name) => {
const cookies = parse(document.cookie)
return cookies[name]
})
return cookie
},
},
})
const {
data: { session },
} = await supabase.auth.getSession()
return { supabase, session }
}
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 functio 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.
I am also adding client logout function for enhance.
// 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: { supabase }
} = 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 supabase.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.supabase.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 async function load({locals: { getSession }}) {
const session = await getSession();
// if the user is already logged in return him to the home page
if (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.supabase.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 async function load({ locals: { getSession } }) {
const session = await getSession();
// if the user is already logged in return him to the home page
if (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.supabase.auth.signOut()
redirect(303, '/');
}
}
// we only use this endpoint for the api
// and don't need to see the 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.supabase.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 async function load({locals: { getSession } }) {
const session = await getSession();
// if the user is already logged in return him to the home page
if (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.supabase.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 async function load({locals: { getSession } }) {
const session = await getSession();
// if the user is not logged in redirect back to the home page
if (!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.supabase.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 async function load({locals: { getSession } }) {
const session = await getSession();
// if the user is not logged in redirect back to the home page
if (!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>
{data.session.user.email}
<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 async function load({locals: { getSession } }) {
const session = await getSession();
// if the user is not logged in redirect back to the home page
if (!session) {
redirect(303, '/');
}
}
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 { redirect } from "@sveltejs/kit"
export const actions = {
delete_user: async ({ locals, request, cookies }) => {
const storageKey = locals.supabase.storageKey
await locals.supabase.rpc('delete_user');
cookies.delete(storageKey, { path: '/' });
redirect(303, "/");
}
}
export async function load({locals: { getSession } }) {
const session = await getSession();
// if the user is not logged in redirect back to the home page
if (!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 using a name "user_profile") with its primary key (id) referencing Authetication table id.
My addUserprofileToUser() function looks like this:
export default async function addUserprofileToUser (session, supabase) {
if (session) {
let { data, error } = await supabase
.from('user_profile')
.select("*")
.eq('id', session.user.id)
.single()
session.user.user_profile = data
}
}
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 and then again in +layout.server.js because for some reason Supbase SSR Auth seems to somehow strip the profile data from the sesson after hooks file runs.
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.
Top comments (28)
Awesome article, thank you!
Have you tried doing OAuth? Any chance you could add this to the article too?
Thank you for your interest.
I definitely plan to look into OAuth as well.
Something I noticed when duplicating a logged-in session on 2 browser tabs. If I log out on the second tab and click around on the first tab, the behavior is 'interesting'.
I am still seeing the same behavior on the browser (logout button is still visible). I added a console.log on onMount and noticed it doesn't get printed when following this sequence of actions.
Not sure if related, I am using TS, and the IDE warns me on the line
onMount(async () => {
that:
Svelte: Argument of type () => Promise<() => void> is not assignable to parameter of type () => (() => any) | Promise<never>
Thank you for your efforts. I'll take a look!
Hey, this video explains this problem well and suggests a solution:
Hope it helps anyone else as well!
To reload the layout view after clicking on a navigation item do this:
This appends a CSS class if the URL does not match the path name.
I have updated the tutorial. Now everything works correctly and in sync. The point was to properly implement invalidation listeninng to supabase.auth.onAuthStateChange.
Please test the app and let me know if it works for you as well.
Let me first say, thank you so much for this awesome article!
I am however wondering about the warning I keep getting:
Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and many not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.
Any ideas how to remove this? All of my attempts have fallen flat.
Also, an OAuth article would be awesome!!!
Thank you!!!
Hi, thank you for your thank you :)
Concerning the warning I recommend to follow Supabase recently updated official documentation. I did not have time to update this tutorial. But I guess as long as you call
supabase.auth
methods withsupabase.auth.getUser()
to check JWT validation.Try this
event.locals.safeGetSession
, see the new documentation hereThanks again for the article. I am still not all the way there though. Everything works as expected in development but when I try to deploy on vercel, I get the error:
RollupError: "PUBLIC_SUPABASE_URL" is not exported by "virtual:$env/static/public", imported by "src/routes/+layout.ts".
Hi, thank you for yout thank you.
Concernig Vercel did you set up all your environment varaibles in Vercel? vercel.com/docs/projects/environme...
Awesome article, you have helped me so much as I am new to the JS world. Please do an OAuth one with Supabase+SK as well! Thanks!!
Glad to hear you did like the tutorial.
Great article! I'm new with sveltekit, Is there a form to avoid duplicate load function for session validation?
Hi, thank you.
I am not sure which duplicate load function do you mean? Could you be more specific, please?
Each +page.server.js is checking if user is already looged in. Is there a way to check it in the +layout.server.js?
I would not recommend using +layout.server.js for auth checking. See more details in this discussion
github.com/sveltejs/kit/issues/6315 or this video https://www.youtube.com/watch?v=UbhhJWV3bmI&ab_channel=Huntabyte
Conrening protected routes and redirects in one place you may follow this advice https://www.youtube.com/watch?v=K1Tya6ovVOI&ab_channel=Huntabyte and move the logic to src/hooks.server.js
Thank you for this post kvetoslanovak!
Works perfectly! 😁😍
Thanks for the great article!
Pls share the ./utils/addUserprofileToUser.js file
Thank you, I am sharing this function in a second edit in the bottom of the article.
just started a new project and decide to use these two; great article!
but I was wondering what the addUserprofileToUser function did?
Thank you, I have added the explanaton as a second edit in the bottom of the article.
Thanks for the tutorial. What would the app.d.ts file look like for those of us in love with typescript?
I am just discussing the update of the official documentaton with Supanase. This documentaton uses TS and can be found here: supabase.com/docs/guides/auth/serv...
In previous documentation for Supabase Auth Helpers there is src/app.d.ts supabase.com/docs/guides/auth/auth...