@rychunpm/auth-helpers-sveltekit v0.6.14
@supabase/auth-helpers-sveltekit (BETA)
This submodule provides convenience helpers for implementing user authentication in SvelteKit applications.
Installation
Using npm:
npm install @supabase/auth-helpers-sveltekit
Using yarn:
yarn add @supabase/auth-helpers-sveltekit
This library supports the following tooling versions:
- Node.js:
^16.15.0
Getting Started
Configuration
Set up the fillowing env vars. For local development you can set them in a .env
file. See an example here.
# Find these in your Supabase project settings > API
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SupabaseClient setup
We will start off by creating a db.ts
file inside of our src/lib
directory. Now lets instantiate our supabaseClient
.
// src/lib/db.ts
import { createClient } from '@supabase/supabase-js';
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/public';
// or use the static env
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
persistSession: false,
autoRefreshToken: false
});
setupSupabaseHelpers({
supabaseClient,
cookieOptions: {
secure: !dev
}
});
Initialize the client
Edit your +layout.svelte
file and set up the client side.
<!-- src/routes/+layout.svelte -->
<script lang="ts">
// we need to make sure the supabase instance is initialized on the client
import '$lib/db';
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
// this sets up automatic token refreshing
startSupabaseSessionSync();
</script>
<slot />
Hooks setup
Our hooks.ts
file is where the heavy lifting of this library happens:
// src/hooks.server.ts
// we need to make sure the supabase instance is initialized on the server
import '$lib/db';
import { dev } from '$app/environment';
import { auth } from '@supabase/auth-helpers-sveltekit/server';
export const handle = auth();
// use the sequence helper if you have additional Handle methods
import { sequence } from '@sveltejs/kit/hooks';
export const handle = sequence(auth(), yourHandler);
There are three handle methods available:
callback()
:This will create a handler for
/api/auth/callback
. Theclient
forwards the session details here every timeonAuthStateChange
fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly.session()
:This will parse the session from the cookie and populate it in locals
auth()
:a shorthand for
sequence(callback(), session())
that uses both handlers
Send session to client
In order to make the session available to the UI (pages, layouts) we need to pass the session in the root layout load function:
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return {
session: locals.session
};
};
Typings
In order to get the most out of TypeScript and it´s intellisense, you should import our types into the app.d.ts
type definition file that comes with your SvelteKit project.
// src/app.d.ts
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Locals {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
interface PageData {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
// interface Error {}
// interface Platform {}
}
Basic Setup
You can now determine if a user is authenticated on the client-side by checking that the user
object in $page.data.session
is defined.
<!-- src/routes/+page.svelte -->
<script>
import { page } from '$app/stores';
</script>
{#if !$page.data.session.user}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {$page.data.session.user.email}</h1>
<p>I am logged in!</p>
{/if}
Client-side data fetching with RLS
For row level security to work properly when fetching data client-side, you need to make sure to import the { supabaseClient }
from $lib/db
and only run your query once the user is defined client-side in $page.data.session
:
<script>
import { supabaseClient } from '$lib/db';
import { page } from '$app/stores';
let loadedData = [];
async function loadData() {
const { data } = await supabaseClient.from('test').select('*').limit(20);
loadedData = data;
}
$: if ($page.data.session.user) {
loadData();
}
</script>
{#if $page.data.session.user}
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/if}
Server-side data fetching with RLS
<!-- src/routes/profile/+page.svelte -->
<script>
/** @type {import('./$types').PageData} */
export let data;
$: ({ user, tableData } = data);
</script>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(tableData, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
For row level security to work in a server environment, you need to use the withAuth
helper to check if the user is authenticated. The helper extends the event with session
and getSupabaseClient()
:
// src/routes/profile/+page.ts
import type { PageLoad } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { redirect } from '@sveltejs/kit';
interface TestTable {
id: string;
created_at: string;
}
export const load: PageLoad = withAuth(async ({ getSupabaseClient, session }) => {
if (!session.user) {
throw redirect(303, '/');
}
const { data: tableData } = await getSupabaseClient()
.from<TestTable>('test')
.select('*');
return {
user: session.user,
tableData
};
);
Caution:
Always use the instance returned by getSupabaseClient()
directly!
// Bad
const supabaseClient = getSupabaseClient();
await supabaseClient.from('table1').select();
await supabaseClient.from('table2').select();
// Good
await getSupabaseClient().from('table1').select();
await getSupabaseClient().from('table2').select();
Protecting API routes
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is null
.
// src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { json, redirect } from '@sveltejs/kit';
interface TestTable {
id: string;
created_at: string;
}
export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
throw redirect(303, '/');
}
const { data } = await getSupabaseClient()
.from<TestTable>('test')
.select('*');
return json({ data });
);
If you visit /api/protected-route
without a valid session cookie, you will get a 303 response.
Protecting Actions
Wrap an Action to check that the user has a valid session. If they're not logged in the session is null
.
// src/routes/posts/+page.server.ts
import type { Actions } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { error, invalid } from '@sveltejs/kit';
export const actions: Actions = {
createPost: withAuth(async ({ session, getSupabaseClient, request }) => {
if (!session.user) {
// the user is not signed in
throw error(403, { message: 'Unauthorized' });
}
// we are save, let the user create the post
const formData = await request.formData();
const content = formData.get('content');
const { error: createPostError, data: newPost } = await getSupabaseClient()
.from('posts')
.insert({ content });
if (createPostError) {
return invalid(500, {
supabaseErrorMessage: createPostError.message
});
}
return {
newPost
};
})
};
If you try to submit a form with the action ?/createPost
without a valid session cookie, you will get a 403 error response.
Saving and deleting the session
Use saveSession
to save the session cookies:
import type { Actions } from './$types';
import { supabaseClient } from '$lib/db';
import { invalid, redirect } from '@sveltejs/kit';
import { saveSession } from '@supabase/auth-helpers-sveltekit/server';
export const actions: Actions = {
async signin({ request, cookies, url }) {
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const { data, error } = await supabaseClient.auth.api.signInWithEmail(
email,
password,
{
redirectTo: `${url.origin}/logging-in`
}
);
if (error || !data) {
if (error?.status === 400) {
return invalid(400, {
error: 'Invalid credentials',
values: {
email
}
});
}
return invalid(500, {
error: 'Server error. Try again later.',
values: {
email
}
});
}
saveSession(cookies, data);
throw redirect(303, '/dashboard');
}
};
Use deleteSession
to delete the session cookies:
import type { Actions } from './$types';
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server';
import { redirect } from '@sveltejs/kit';
export const actions: Actions = {
async logout({ cookies }) {
deleteSession(cookies);
throw redirect(303, '/');
}
};
Custom session namespace
If you wan´t to use something else than locals.session
and $page.data.session
you can do so by updating the types and creating three helper functions:
// src/app.d.ts
declare namespace App {
interface Locals {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
interface PageData {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
}
// src/hooks.server.ts
setupSupabaseServer({
supabaseClient,
cookieOptions: {
secure: !dev
},
// --- change location within locals ---
getSessionFromLocals: (locals) => locals.mySupabaseSession,
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session)
});
// src/lib/db.ts
setupSupabaseClient({
supabaseClient,
// --- change location within pageData ---
getSessionFromPageData: (data) => data.mySupabaseSession
});