This repository demonstrates integrating Supabase with SvelteKit.
This project requires your Supbase URL and public token
VITE_SUPABASE_URL=https://xxxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=<jwt_token>
To get started create a Supabase account and run the following script in the SQL tab of a new project:
create table todos (
id bigint generated by default as identity primary key,
user_id uuid references auth.users not null,
task text check (char_length(task) > 3),
is_complete boolean default false,
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table todos enable row level security;
create policy "Individuals can create todos." on todos for
insert with check (auth.uid() = user_id);
create policy "Individuals can view their own todos. " on todos for
select using (auth.uid() = user_id);
create policy "Individuals can update their own todos." on todos for
update using (auth.uid() = user_id);
create policy "Individuals can delete their own todos." on todos for
delete using (auth.uid() = user_id);
I have OAuth integration with Github and Twitter configured. To set this up follow the documentation here.
In order to make authenticated calls to Supabase within endpoints (server-side) we need to send the JWT to the server and respond with a cookie. We can do this in the auth.onAuthStateChange()
callback:
import { supabase } from '$lib/supabaseClient';
import { session } from '$app/stores';
import { setAuthCookie } from '$lib/utils/session';
import Header from '$lib/Header/index.svelte';
import '../app.css';
// this should probably live in your global __layout.svelte file
supabase.auth.onAuthStateChange(async (event, _session) => {
if (event !== 'SIGNED_OUT') {
session.set({ user: _session.user });
await setAuthCookie(_session);
} else {
session.set({ user: { guest: true } });
await unsetAuthCookie();
}
});
The setAuthCookie
makes a request to an endpoint with the JWT and responds with a cookie:
// setAuthCookie
export async function setServerSession(event, session) {
await fetch('/api/auth.json', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session })
});
}
export const setAuthCookie = async (session) => await setServerSession('SIGNED_IN', session);
It's important to include the event as the supabase-js
library expects it.
Our endpoint to set the cookie looks like this:
export async function post(req /*, res: Response (read the notes below) */) {
// Unlike, Next.js API handlers you don't get the response object here. As a result, you cannot invoke the below method to set cookies on the responses.
// await supabaseClient.auth.api.setAuthCookie(req, res);
// `supabaseClient.auth.api.setAuthCookie(req, res)` is dependent on both the request and the responses
// `req` used to perform few validations before setting the cookies
// `res` is used for setting the cookies
return {
status: 200,
body: null
};
}
You're probably thinking "where is the cookie set?" - that has to be done in a hook
because we don't have access to the response object to pass to the supabase.auth.setAuthCookie
function.
Our hooks.js
file then looks like this:
export const handle = async ({ request, resolve }) => {
// Parses `req.headers.cookie` adding them as attribute `req.cookies, as `auth.api.getUserByCookie` expects parsed cookies on attribute `req.cookies`
const expressStyleRequest = toExpressRequest(request);
// We can then fetch the authenticated user using this cookie
const { user } = await auth.api.getUserByCookie(expressStyleRequest);
// Add the user and the token to our locals so they are available on all SSR pages
request.locals.token = expressStyleRequest.cookies['sb:token'] || undefined;
request.locals.user = user || { guest: true };
// If we have a token, set the supabase client to use it so we can make authorized requests as that user
if (request.locals.token) {
supabase.auth.setAuth(request.locals.token);
}
//...<snip>
let response = await resolve(request);
// if auth request - set cookie in response headers
if (request.method == 'POST' && request.path === '/api/auth.json') {
auth.api.setAuthCookie(request, toExpressResponse(response));
response = toSvelteKitResponse(response);
}
return response;
};
It's probably possible to do this in the endpoint itself as we can set cookies on the response object of an endpoint:
const token = "token"
const cookie = `sb:token=${token}; Path=/; Secure; HttpOnly; Max-Age=2592000; etc...`
return {
headers: {
'set-cookie': [cookie]
}
}
The reason I would not do this is that if the client javascript library changes then we will have to update our endpoint to match.
This works but it isn't ideal for the following reasons:
- Supabase stores the refresh token in
localstorage
which makes it vulnerable to XSS. This is done so thesupabase-js
client library can refresh the token on the users behalf. - There's no refresh mechanism on a SSR request.
- It doesn't follow the best practices for JWTs on frontend clients.
Any improvement will likely make Supbase Auth more difficult to use, so what I'm proposing would be opt in for users that want to improve their auth security.
- Allow configuration of the refresh endpoint so we can proxy the request to Supabase ourselves. This would allow us to set a refresh token in a cookie and remove it from localstorage. There would need to be some extra security work in order to make sure a client wasn't sending the request directly to a Supabase auth endpoint.
- Expose a function to generate the cookie using just the token. This would save us having to generate a fake Express-style request/response just to get the cookie.
Once you've created a project and installed dependencies with npm install
(or pnpm install
or yarn
), start a development server:
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
Before creating a production version of your app, install an adapter for your target environment. Then:
npm run build
You can preview the built app with
npm run preview
, regardless of whether you installed an adapter. This should not be used to serve your app in production.