Files
supabase/apps/docs/content/guides/auth/server-side/sveltekit.mdx
T
Stojan DimitrovskiandChris Chinchilla 93ba2a312c docs: indicate publishable key instead of anon in many examples (#37411)
* docs: indicate publishable key instead of anon in many examples

* replace your-anon-key to string indicating publishable or anon

* fix your_...

* apply suggestion from @ChrisChinchilla

Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>

* Update keys in code examples

* Prettier fix

* Update apps/docs/content/guides/functions/schedule-functions.mdx

---------

Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>
2025-08-18 13:47:48 +02:00

648 lines
16 KiB
Plaintext

---
title: 'Setting up Server-Side Auth for SvelteKit'
hideToc: true
---
Set up Server-Side Auth to use cookie-based authentication with SvelteKit.
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Install Supabase packages">
Install the `@supabase/supabase-js` package and the helper `@supabase/ssr` package.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sh
npm install @supabase/supabase-js @supabase/ssr
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Set up environment variables">
Create a `.env.local` file in your project root directory.
Fill in your `PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_PUBLISHABLE_KEY`:
<ProjectConfigVariables variable="url" />
<ProjectConfigVariables variable="anonKey" />
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```txt name=.env.local
PUBLIC_SUPABASE_URL=<your_supabase_project_url>
PUBLIC_SUPABASE_PUBLISHABLE_KEY=<sb_publishable_... or anon keyY>
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Set up server-side hooks">
Set up server-side hooks in `src/hooks.server.ts`. The hooks:
- Create a request-specific Supabase client, using the user credentials from the request cookie. This client is used for server-only code.
- Check user authentication.
- Guard protected pages.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/hooks.server.ts
import { createServerClient } from '@supabase/ssr'
import { type Handle, redirect } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'
const supabase: Handle = async ({ event, resolve }) => {
/**
* Creates a Supabase client specific to this server request.
*
* The Supabase client gets the Auth token from the request cookies.
*/
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
cookies: {
getAll: () => event.cookies.getAll(),
/**
* SvelteKit's cookies API requires `path` to be explicitly set in
* the cookie options. Setting `path` to `/` replicates previous/
* standard behavior.
*/
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: '/' })
})
},
},
})
/**
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
* JWT before returning the session.
*/
event.locals.safeGetSession = async () => {
const {
data: { session },
} = await event.locals.supabase.auth.getSession()
if (!session) {
return { session: null, user: null }
}
const {
data: { user },
error,
} = await event.locals.supabase.auth.getUser()
if (error) {
// JWT validation has failed
return { session: null, user: null }
}
return { session, user }
}
return resolve(event, {
filterSerializedResponseHeaders(name) {
/**
* Supabase libraries use the `content-range` and `x-supabase-api-version`
* headers, so we need to tell SvelteKit to pass it through.
*/
return name === 'content-range' || name === 'x-supabase-api-version'
},
})
}
const authGuard: Handle = async ({ event, resolve }) => {
const { session, user } = await event.locals.safeGetSession()
event.locals.session = session
event.locals.user = user
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
redirect(303, '/auth')
}
if (event.locals.session && event.url.pathname === '/auth') {
redirect(303, '/private')
}
return resolve(event)
}
export const handle: Handle = sequence(supabase, authGuard)
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Create TypeScript definitions">
To prevent TypeScript errors, add type definitions for the new `event.locals` properties.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/app.d.ts
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
import type { Database } from './database.types.ts' // import generated types
declare global {
namespace App {
// interface Error {}
interface Locals {
supabase: SupabaseClient<Database>
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
session: Session | null
user: User | null
}
interface PageData {
session: Session | null
}
// interface PageState {}
// interface Platform {}
}
}
export {}
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Create a Supabase client in your root layout">
Create a Supabase client in your root `+layout.ts`. This client can be used to access Supabase from the client or the server. In order to get access to the Auth token on the server, use a `+layout.server.ts` file to pass in the session from `event.locals`.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/routes/+layout.ts
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import type { LayoutLoad } from './$types'
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
/**
* Declare a dependency so the layout can be invalidated, for example, on
* session refresh.
*/
depends('supabase:auth')
const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
global: {
fetch,
},
})
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
global: {
fetch,
},
cookies: {
getAll() {
return data.cookies
},
},
})
/**
* It's fine to use `getSession` here, because on the client, `getSession` is
* safe, and on the server, it reads `session` from the `LayoutData`, which
* safely checked the session using `safeGetSession`.
*/
const {
data: { session },
} = await supabase.auth.getSession()
const {
data: { user },
} = await supabase.auth.getUser()
return { session, supabase, user }
}
```
```ts name=src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
const { session } = await safeGetSession()
return {
session,
cookies: cookies.getAll(),
}
}
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Listen to Auth events">
Set up a listener for Auth events on the client, to handle session refreshes and signouts.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```svelte name=src/routes/+layout.svelte
<script>
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
let { data, children } = $props()
let { session, supabase } = $derived(data)
onMount(() => {
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
if (newSession?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
})
return () => data.subscription.unsubscribe()
})
</script>
{@render children()}
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={7}>
<StepHikeCompact.Details title="Create your first page">
Create your first page. This example page calls Supabase from the server to get a list of colors from the database.
This is an example of a public page that uses publicly readable data.
To populate your database, run the [colors quickstart](/dashboard/project/_/sql/quickstarts) from your dashboard.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/routes/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ locals: { supabase } }) => {
const { data: colors } = await supabase.from('colors').select('name').limit(5).order('name')
return { colors: colors ?? [] }
}
```
```svelte name=src/routes/+page.svelte
<script>
let { data } = $props()
let { colors } = $derived(data)
</script>
<h1>Welcome to Supabase!</h1>
<ul>
{#each colors as color}
<li>{color.name}</li>
{/each}
</ul>
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={8}>
<StepHikeCompact.Details title="Change the Auth confirmation path">
If you have email confirmation turned on (the default), a new user will receive an email confirmation after signing up.
Change the email template to support a server-side authentication flow.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={9}>
<StepHikeCompact.Details title="Create a login page">
Next, create a login page to let users sign up and log in.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/routes/auth/+page.server.ts
import { redirect } from '@sveltejs/kit'
import type { Actions } from './$types'
export const actions: Actions = {
signup: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signUp({ email, password })
if (error) {
console.error(error)
redirect(303, '/auth/error')
} else {
redirect(303, '/')
}
},
login: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
console.error(error)
redirect(303, '/auth/error')
} else {
redirect(303, '/private')
}
},
}
```
```svelte name=src/routes/auth/+page.svelte
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
<button>Login</button>
<button formaction="?/signup">Sign up</button>
</form>
```
```svelte name=src/routes/auth/+layout.svelte
<script>
let { children } = $props()
</script>
<header>
<nav>
<a href="/">Home</a>
</nav>
</header>
{@render children()}
```
```svelte name=src/routes/auth/error/+page.svelte
<p>Login error</p>
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={10}>
<StepHikeCompact.Details title="Create the signup confirmation route">
Finish the signup flow by creating the API route to handle email verification.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/routes/auth/confirm/+server.ts
import type { EmailOtpType } from '@supabase/supabase-js'
import { redirect } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
const token_hash = url.searchParams.get('token_hash')
const type = url.searchParams.get('type') as EmailOtpType | null
const next = url.searchParams.get('next') ?? '/'
/**
* Clean up the redirect URL by deleting the Auth flow parameters.
*
* `next` is preserved for now, because it's needed in the error case.
*/
const redirectTo = new URL(url)
redirectTo.pathname = next
redirectTo.searchParams.delete('token_hash')
redirectTo.searchParams.delete('type')
if (token_hash && type) {
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
if (!error) {
redirectTo.searchParams.delete('next')
redirect(303, redirectTo)
}
}
redirectTo.pathname = '/auth/error'
redirect(303, redirectTo)
}
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={11}>
<StepHikeCompact.Details title="Create private routes">
Create private routes that can only be accessed by authenticated users. The routes in the `private` directory are protected by the route guard in `hooks.server.ts`.
To ensure that `hooks.server.ts` runs for every nested path, put a `+layout.server.ts` file in the `private` directory. This file can be empty, but must exist to protect routes that don't have their own `+layout|page.server.ts`.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```ts name=src/routes/private/+layout.server.ts
/**
* This file is necessary to ensure protection of all routes in the `private`
* directory. It makes the routes in this directory _dynamic_ routes, which
* send a server request, and thus trigger `hooks.server.ts`.
**/
```
```svelte name=src/routes/private/+layout.svelte
<script>
let { data, children } = $props()
let { supabase } = $derived(data)
const logout = async () => {
const { error } = await supabase.auth.signOut()
if (error) {
console.error(error)
}
}
</script>
<header>
<nav>
<a href="/">Home</a>
</nav>
<button onclick={logout}>Logout</button>
</header>
<main>
{@render children()}
</main>
```
```sql name=SQL
-- Run this SQL against your database to create a `notes` table.
create table notes (
id bigint primary key generated always as identity,
created_at timestamp with time zone not null default now(),
user_id uuid references auth.users on delete cascade not null default auth.uid(),
note text not null
);
alter table notes enable row level security;
revoke all on table notes from authenticated;
revoke all on table notes from anon;
grant all (note) on table notes to authenticated;
grant select (id) on table notes to authenticated;
grant delete on table notes to authenticated;
create policy "Users can access and modify their own notes"
on notes
for all
to authenticated
using ((select auth.uid()) = user_id);
```
```svelte name=src/routes/private/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ depends, locals: { supabase } }) => {
depends('supabase:db:notes')
const { data: notes } = await supabase.from('notes').select('id,note').order('id')
return { notes: notes ?? [] }
}
```
```svelte name=src/routes/private/+page.svelte
<script lang="ts">
import { invalidate } from '$app/navigation'
import type { EventHandler } from 'svelte/elements'
import type { PageData } from './$types'
let { data } = $props()
let { notes, supabase, user } = $derived(data)
const handleSubmit: EventHandler<SubmitEvent, HTMLFormElement> = async (evt) => {
evt.preventDefault()
if (!evt.target) return
const form = evt.target as HTMLFormElement
const note = (new FormData(form).get('note') ?? '') as string
if (!note) return
const { error } = await supabase.from('notes').insert({ note })
if (error) console.error(error)
invalidate('supabase:db:notes')
form.reset()
}
</script>
<h1>Private page for user: {user?.email}</h1>
<h2>Notes</h2>
<ul>
{#each notes as note}
<li>{note.note}</li>
{/each}
</ul>
<form onsubmit={handleSubmit}>
<label>
Add a note
<input name="note" type="text" />
</label>
</form>
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>