I may pivot Supakit into something else in the future, but for now you should consider this package to be deprecated.
- If you'd like to use httpOnly cookies, I have this repo branch as a guide.
- The callback and confirm routes require minimal code.
- There is still no Supabase-native way to save the provider tokens.
- The legacy SvelteKit session store is long past its end of life, and continuing to implement would be a rare requirement.
You can reference the main
branch of my repo, https://github.com/j4w8n/sveltekit-supabase-ssr, for details on implementing Supabase SSR with SvelteKit.
A Supabase auth helper for SvelteKit.
v2.0.0 is breaking, as it requires the following:
- @sveltejs/kit ^2.4.1
- svelte >=4.2.9
- vite ^5.0.12
- Uses
httpOnly
cookie storage, for tighter security against XSS. This includes CSRF protection for the endpoints that Supakit creates.1 - "Remember Me" feature.
- Option to set
flowType
anddebug
for client auth. - Provides a callback route for server-side auth, so you don't have to setup
exchangeCodeForSession()
. - Provides a confirm route for server-side token hash otp verification.
- Built-in server client, again less setup for you.
- Saves the
provider_token
andprovider_refresh_token
in their ownhttpOnly
cookies. These values are also available inevent.locals.session
. Please note that Supakit will not refresh these tokens for you. - Offers a secure client-side "session" store, which is hydrated with Supabase session info after most auth events. This helps with immediate reactivity after these events occur. The
invalidate()
method is an alternative to this. - Opt-out of server-side features (client, event.locals); but very uncommon.
npm install supakit
pnpm add supakit
yarn add supakit
For the examples, we are assuming that you use Typescript and are generating database types to $lib/database.d.ts
Create an .env
file in the root of your project, with your PUBLIC_SUPABASE_URL
and PUBLIC_SUPABASE_ANON_KEY
values; and/or ensure these are set on your deployment platform.
Per Supabase docs, you can also add your database types to your client. For other options, beside linking, checkout the docs.
In your local CLI:
supabase link --project-ref <project-id>
supabase gen types typescript --linked > src/lib/database.d.ts
Ensure your app.d.ts file includes the following.
cookie_options
is only needed if you plan to set additional cookies on the server-side and wanna use Supakit's cookie options; or if you pass in custom cookie options. See Cookie Options to learn how to change the defaults.
/* src/app.d.ts */
import { SupabaseClient, Session } from '@supabase/supabase-js'
import { Database } from '$lib/database.d'
import { SecureCookieOptionsPlusName } from 'supakit'
declare global {
namespace App {
interface Locals {
cookie_options: SecureCookieOptionsPlusName;
session: Session | null;
supabase: SupabaseClient<Database>;
}
interface PageData {
session: Session | null;
supabase: SupabaseClient<Database>;
}
}
}
Create a Supabase client in your root +layout.ts file. We're using $env/dynamic
in the example, but you can also use $env/static
if it's a better fit for your use-case.
This client will now be available in either data
or $page.data
in your downstream load functions and pages.
Pass in an object of Supabase Client Options as the fourth parameter to createSupabaseLoadClient
. Any options you pass in here, you'll want to setup for the server client as well.
Since Supakit only allows certain auth options, we've included them below. Type defaults are shown as the last option.
Auth Types:
flowType?: 'implicit' | 'pkce'
debug?: true | false
If you want to use SvelteKit's native invalidate method, after session changes, be sure to use depends
below. Otherwise, you can omit and setup the session store.
/* src/routes/layout.ts */
import { env } from '$env/dynamic/public'
import { createSupabaseLoadClient } from 'supakit'
import type { Database } from '$lib/database.d'
export const load = async ({ data: { session }, depends }) => {
depends('supabase:auth')
const supabase = createSupabaseLoadClient<Database>(
env.PUBLIC_SUPABASE_URL,
env.PUBLIC_SUPABASE_ANON_KEY,
session
)
return { supabase, session }
}
Listen for auth changes in your root +layout.svelte file. You'll need to pass-in your Supabase load client as the first parameter.
If using depends
to invalidate data after session changes, be sure to setup the callback function. Otherwise,
reference auth state to use Supakit's session store.
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { supabaseAuthStateChange } from 'supakit'
import { onMount } from 'svelte'
export let data
onMount(() => {
supabaseAuthStateChange(data.supabase, null, ({ event, session }) => {
if (data.session.expires_at !== session.expires_at) invalidate('supabase:auth')
}))
})
</script>
Handles endpoints, setting event.locals
, and initializing the Supabase server client.
/* src/hooks.server.ts */
import { supakit } from 'supakit'
export const handle = supakit
If you don't want Supakit to handle server-side features like populating event.locals
and creating a server client, then you can use a different import which will only handle setting cookies and the server-side auth callback.
/* src/hooks.server.ts */
import { supakitLite } from 'supakit'
export const handle = supakitLite
There's nothing for you to do here, just a heads-up that these are available to use:
/* Session info from Supabase */
event.locals.session = {
provider_token,
provider_refresh_token,
access_token,
refresh_token,
expires_in,
expires_at,
token_type,
user
}
/* Supakit's server-side Supabase client */
event.locals.supabase
/**
* If you want to set your own cookies on the server-side,
* you can use Supakit's default options - or your custom
* options, if you've set them (see further below).
*/
event.locals.cookie_options
expires_in
will get calculated, and reflect how many seconds are left until youraccess_token
expires.expires_at
is taken directly from the jwt. Keep in mind that these two values are only updated when thehandle
function is called inhooks.server.ts
; so don't rely on them for realtime info.
The built-in Supabase server client relies on $env/dynamic/public
. If there is a logged-in user, they're automatically "signed in" to this client.
/* some server-side load file, for example src/routes/+layout.server.ts */
export const load = ({ locals: { session, supabase } }) => {
const { data, error } = await supabase.from('table').select('column')
return {
stuff: data,
session
}
}
If you'd like to set client options for the server client, pass them into a function at the root of your server hooks. You can reference them at SupabaseClientOptions.
Since Supakit only allows certain auth options, we've included them below. Type defaults are shown as the last option.
Auth Types:
flowType?: 'implicit' | 'pkce'
debug?: true | false
There's no compelling reason to set flowType
to implicit
for a server client, because any implicit flows would happen in the browser.
/* src/hooks.server.ts */
import { supakit, setSupabaseServerClientOptions } from 'supakit'
setSupabaseServerClientOptions({
client_options: {
auth: {
debug: true
}
}
})
export const handle = supakit
Supakit provides an endpoint for handling the exchangeCodeForSession
method; so there's no need to create this route yourself. You can also append the redirectTo url with a next
parameter for post-auth redirects, e.g./app
.
Here we show an example using page actions.
/* some server-side file, like src/routes/login/+page.server.ts */
export const actions = {
signin: async ({ request, url, locals: { supabase } }) => {
const data = await request.formData()
const provider = data.get('provider') as Provider
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${url.origin}/supakit/callback?next=/app`
}
})
/* handle error */
if (data.url) throw redirect(303, data.url)
}
}
<!-- some client-side file, like src/routes/login/+page.svelte -->
<form method="POST" action="?/signin">
<button name="provider" value="github">GitHub</button>
<button name="provider" value="google">Google</button>
</form>
This method can be used for the following purposes:
- Confirm a signup
- Invite a user
- Magic Link email signin
- Reset a password
Be sure to change your email templates per the Supabase guide. When doing so, change any references of /auth/confirm
to /supakit/confirm
. Also, change any next
parameter to the appropriate page on your site.
Magic Link email Example
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com'
})
Reset a password Example
const { data, error } = await supabase.auth.resetPasswordForEmail({
email: 'user@example.com'
})
If you'd rather sign-in on the client-side, set your client's flowType
to implicit
and use typical Supabase sign-in methods.
Load client example:
/* src/routes/layout.ts */
import { env } from '$env/dynamic/public'
import { createSupabaseLoadClient } from 'supakit'
import type { Database } from '$lib/database.d'
export const load = async ({ data: { session } }) => {
const supabase = createSupabaseLoadClient<Database>(
env.PUBLIC_SUPABASE_URL,
env.PUBLIC_SUPABASE_ANON_KEY,
session, {
auth: {
flowType: 'implicit'
}
}
)
return { supabase, session }
}
getSessionStore()
is a secure, session store using Svelte's context API. If you pass the store into supabaseAuthStateChange()
, Supakit will automatically hydrate the store with the returned Supabase session
info after the INITIAL_SESSION
, SIGNED_IN
, SIGNED_OUT
, TOKEN_REFRESHED
, and USER_UPDATED
events - giving you immediate reactivity for any part of your app that relies on the value of the store.
To hydrate the store during initial load and page refreshes, you can populate the store with returned server data.
Setup
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { onMount } from 'svelte'
import { getSessionStore, supabaseAuthStateChange } from 'supakit'
export let data
const session_store = getSessionStore()
/**
* data.session assumes you return `session` from a file
* like src/routes/+layout.server.ts with code such as:
* return {
* session: event.locals.session
* }
*
* and then from src/routes/+layout.ts with code such as:
* return {
* session: data.session
* }
*/
$session_store = data.session
onMount(() => {
supabaseAuthStateChange(data.supabase, session_store)
})
</script>
Page Usage
<!-- /src/routes/some/path/+page.svelte -->
<script lang="ts">
import { getSessionStore } from 'supakit'
const session_store = getSessionStore()
</script>
{#if $session_store}
<h4>Your id is {$session_store.user.id}</h4>
{/if}
supabaseAuthStateChange()
handles logic for Supabase's onAuthStateChange()
. A Supabase client is required to be passed-in. Then it takes an optional Svelte store, and a callback function. The callback function receives the Supabase { event, session }
object as a parameter, for doing additional work after an auth event.
If you pass in a store, Supakit will hydrate it with the Supabase session after the INITIAL_SESSION
, SIGNED_IN
, SIGNED_OUT
, TOKEN_REFRESHED
, and USER_UPDATED
events. This option is usually used in lieu of SvelteKit's invalidate()
and depends()
functions. Using both methods would be uncommon.
Type:
supabaseAuthStateChange(
client: SupabaseClient,
store?: Writable<Session | null> | null,
callback?: (({ event, session }: { event: string, session: Session | null }) => void)
)
Example:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { goto, invalidate } from '$app/navigation'
import { onMount } from 'svelte'
import { getSessionStore, supabaseAuthStateChange } from 'supakit'
export let data
const session_store = getSessionStore()
/**
* data.session assumes you return `session` from a file
* like src/routes/+layout.server.ts or +layout.ts with code such as:
* return {
* session: event.locals.session
* }
*/
$session_store = data.session
onMount(() => {
supabaseAuthStateChange(data.supabase, session_store, ({ event, session }) => {
/**
* Put any post-event code here.
*
* e.g. If you don't use Supakit's session store, then you'll likely want
* to add the below; as well as change `session_store`, above, to `null`.
*/
if (data.session.expires_at !== session.expires_at) invalidate('supabase:auth')
})
})
</script>
Supakit will set upto four cookies.
sb-<supabase_project_id>-auth-token
sb-provider-token
sb-provider-refresh-token
sb-<crypto.randomUUID()>-csrf
sb-<supabase_project_id>-auth-token
, or your custom storage key, is updated after the INITIAL_SESSION
, SIGNED_IN
, SIGNED_OUT
, TOKEN_REFRESHED
, and USER_UPDATED
events. The provider cookies will only be set after the initial SIGNED_IN
event, and will need to be refreshed and updated by you. The csrf cookie is a session cookie, used to help secure the /supakit/cookie
endpoint for cookie storage; and you may notice more than one.
Supakit sets httpOnly cookies by making route requests to the server, then returning a response with the set-cookie
header. These requests are handled programmatically by Supakit. There is no need for you to create routes for this purpose.
Supakit maintains a session cache inside it's custom CookieStorage
for the Supabase load client. When retrieving a user session, for a call like supabase.auth.getSession()
for example, Supakit will return the cached response as long as the session is not null
; saving a trip to the server.
Supakit uses the special, programmed routes
/supakit/cookie
and/supakit/csrf
. Therefore, you should not have a top-level route with the same name (not that anyone would).
Because Supakit uses secure httpOnly cookie storage: setting, getting, and deleting the session requires a request to the server. This causes a delay between sending the request, receiving the response, and finally setting the cookie in the browser. This can cause a timing issue after a user logs in for the first time; specifically if you have callback code for supabaseAuthStateChange()
. To work around this, Supakit will set a non-httpOnly cookie which expires in 5 seconds. This allows any affected code to send the temporary cookie sb-temp-session
to the server, so that the server will know someone is logged in. This cookie exists in the browser, until it's closed; but once it has expired, it cannot be accessed via XSS attacks.
You can set your own options by passing in an object of SecureCookieOptions
plus name
for a custom cookie storage key. Whatever you pass in will be merged with the defaults - overriding when appropriate. Cookie options should be the same for the Load client and Server client.
This is the fifth parameter for createSupabaseLoadClient
.
Type:
type SecureCookieOptions = Omit<CookieSerializeOptions, "httpOnly"> & { name?: string }
Supakit Defaults:
{
path: '/',
maxAge: 60 * 60 * 24 * 365 // one year
}
Load client example:
/* src/routes/layout.ts */
import { env } from '$env/dynamic/public'
import { createSupabaseLoadClient } from 'supakit'
import type { Database } from '$lib/database.d'
export const load = async ({ data: { session } }) => {
const supabase = createSupabaseLoadClient<Database>(
env.PUBLIC_SUPABASE_URL,
env.PUBLIC_SUPABASE_ANON_KEY,
session,
{}, /* Supabase client options is the fourth parameter */
{
maxAge: 60 * 60 * 24 * 365 * 100,
sameSite: 'strict',
name: 'your-custom-storage-key' // replaces `sb-<supabase_project_id>-auth-token`
}
)
return { supabase, session }
}
Server client example:
/* src/hooks.server.ts */
import { supakit, setSupabaseServerClientOptions } from 'supakit'
setSupabaseServerClientOptions({
cookie_options: {
maxAge: 180
}
})
export const handle = supakit
By default SvelteKit sets
httpOnly
andsecure
totrue
, andsameSite
tolax
. Supakit relies on thehttpOnly
value to betrue
for better cookie security. Typescript will show an error if you try to pass it in.
If you need to set your own cookies, you can import getSupabaseLoadClientCookieOptions
on the client-side; or by using event.locals.cookie_options
on the server-side.
Examples:
/* client-side */
import { getSupabaseLoadClientCookieOptions } from 'supakit'
const cookie_options = getSupabaseLoadClientCookieOptions()
/* some server-side file with locals available - example here is a hooks handler */
export const yourHandler = (async ({ event, resolve }) => {
if ('some check') {
const response = new Response(null)
response.headers.append('set-cookie', event.cookies.serialize(`cookie-name`, token, event.locals.cookie_options))
return response
}
return await resolve(event)
}) satisfies Handle
Determines if Supakit remembers a logged-in user or if they are logged out after closing the browser. Defaults to true, whether you use the feature or not.
You can give users the option by adding a checkbox input to your login form, then importing and using the function.
/* src/routes/login/+page.svelte */
<script lang="ts">
import { rememberMe } from 'supakit'
import { onMount } from 'svelte'
let checked: boolean
onMount(() => {
checked = rememberMe().value
})
</script>
<form method="POST" action="?/signin">
<button name="provider" value="github">Login with GitHub</button>
<input
type="checkbox" id="rememberme"
bind:checked
on:click={ () => checked = rememberMe().toggle }/>
<label for="rememberme">Remember Me</label>
</form>
You can pass the debug
option to your clients and see verbose logs for auth.
export const load = async ({ data: { session } }) => {
const supabase = createSupabaseLoadClient<Database>(
env.PUBLIC_SUPABASE_URL,
env.PUBLIC_SUPABASE_ANON_KEY,
session,
{
auth: {
debug: true
}
}
)
return { supabase, session }
}
Sometimes you want a user to be logged in, in order to access certain pages.
Here is our example file structure, where routes /admin
and /app
should be protected. We place these routes under a layout group, so they can share a +layout.server.ts
file. However, this isn't strictly required unless you need shared data across pages under the group.
src/routes/
├ (auth)/
│ ├ admin/
│ │ ├ +page.server.ts
│ │ └ +page.svelte
│ ├ app/
│ │ ├ +page.server.ts
│ │ └ +page.svelte
│ ├ +layout.server.ts
│ └ +layout.svelte
├ login/
│ └ +page.svelte
├ +error.svelte
├ +layout.server.ts
├ +layout.svelte
└ +page.svelte
When using a +layout.server.ts
file, check for a null locals.session
. If there's no session, then the server client won't be authenticated with a user and will act as an anon client.
/* src/routes/(auth)/+layout.server.ts */
import { redirect } from '@sveltejs/kit'
export const load = async ({ locals: { session, supabase } }) => {
if (!session) throw redirect(307, '/login')
/* grab info to return */
let { data, error } = await supabase.from('table').select('column')
return {
stuff: data,
session
}
}
Protect pages using a +page.server.ts
file for each page route. This is needed because +layout.server.ts
will not necessarily run on every request. This method works for both client-side and server-side requests.
/* src/routes/(auth)/app/+page.server.ts */
import { redirect } from '@sveltejs/kit'
export const load = async ({ locals: { session } }) => {
if (!session) throw redirect(307, '/login')
}