11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient, Session } from '@supabase/supabase-js'
2 | import { Database } from './DatabaseDefinitions'
3 |
4 | declare global {
5 | namespace App {
6 | interface Locals {
7 | supabase: SupabaseClient
8 | getSession(): Promise
9 | }
10 | interface PageData {
11 | session: Session | null
12 | supabase: SupabaseClient
13 | }
14 | // interface Error {}
15 | // interface Platform {}
16 | }
17 | }
18 |
19 | export {}
20 |
--------------------------------------------------------------------------------
/src/routes/auth/callback/+server.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from '@sveltejs/kit'
2 |
3 | export const GET = async ({ url, locals: { supabase } }) => {
4 | const code = url.searchParams.get('code') as string
5 | const next = url.searchParams.get('next') ?? '/'
6 |
7 | if (code) {
8 | const { error } = await supabase.auth.exchangeCodeForSession(code)
9 | if (!error) {
10 | redirect(303, `/${next.slice(1)}`)
11 | }
12 | }
13 |
14 | /* Return the user to an error page with instructions */
15 | redirect(303, '/')
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/server/event.ts:
--------------------------------------------------------------------------------
1 | import { getRequestEvent } from "$app/server"
2 |
3 | /**
4 | * Returns the formData items you request,
5 | * from an `event`.
6 | *
7 | * @example const { email, password } = await getFormData('email', 'password')
8 | */
9 | export const getFormData = async <
10 | T = string,
11 | K extends string = string
12 | >(...items: K[]): Promise<{ [key in K]: T | null }> => {
13 | const { request } = getRequestEvent()
14 | const data = await request.formData()
15 | const result: { [key: string]: T | null } = {}
16 |
17 | for (const i of items.values()) {
18 | result[i] = data.get(i) as T
19 | }
20 |
21 | return result as { [key in K]: T | null }
22 | }
23 |
--------------------------------------------------------------------------------
/src/routes/auth/confirm/+server.ts:
--------------------------------------------------------------------------------
1 | import type { EmailOtpType } from '@supabase/supabase-js'
2 | import { redirect } from '@sveltejs/kit'
3 |
4 | export const GET = async ({ url, locals: { supabase } }) => {
5 | const token_hash = url.searchParams.get('token_hash') as string
6 | const type = url.searchParams.get('type') as EmailOtpType
7 | const next = url.searchParams.get('next') ?? '/'
8 |
9 | if (token_hash && type) {
10 | const { error } = await supabase.auth.verifyOtp({ token_hash, type })
11 | if (!error) {
12 | redirect(303, `/${next.slice(1)}`)
13 | }
14 | }
15 |
16 | /* Return the user to an error page with some instructions */
17 | redirect(303, '/')
18 | }
19 |
--------------------------------------------------------------------------------
/src/routes/(authenticated)/app/+page.server.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Auth validation happens in hooks.server.ts, so there's
3 | * no need to check anything here. Plus, we aren't returning
4 | * server data to the /app page; so, this file only exists
5 | * to trigger a server call for client-side routing, to check authentication.
6 | *
7 | * If you have a one-off situation for authentication,
8 | * or you'd rather be more explicit, check for a session and redirect.
9 | *
10 | * import { redirect } from '@sveltejs/kit'
11 | *
12 | * export const load = async ({ locals: { getSession } }) => {
13 | * const session = await getSession()
14 | * if (!session) redirect(307, '/auth')
15 | * }
16 | */
17 | export const load = () => null
18 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto'
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter()
15 | }
16 | }
17 |
18 | export default config
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sveltekit-supabase-ssr",
3 | "version": "0.16.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite build && vite preview",
9 | "debug": "vite --force",
10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
12 | },
13 | "devDependencies": {
14 | "@sveltejs/adapter-auto": "^6.0.1",
15 | "@sveltejs/kit": "^2.25.1",
16 | "@sveltejs/vite-plugin-svelte": "^5.1.1",
17 | "svelte": "^5.36.13",
18 | "svelte-check": "^4.3.0",
19 | "tslib": "^2.8.1",
20 | "typescript": "^5.8.3",
21 | "vite": "^6.3.5"
22 | },
23 | "type": "module",
24 | "dependencies": {
25 | "@supabase/ssr": "^0.6.1",
26 | "@supabase/supabase-js": "^2.52.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
2 | import { getValidatedSession } from '$lib/utils.js'
3 | import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
4 |
5 | export const load = async ({ fetch, data, depends }) => {
6 | depends('supabase:auth')
7 |
8 | const supabase = isBrowser()
9 | ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
10 | global: { fetch }
11 | })
12 | : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
13 | global: { fetch },
14 | cookies: {
15 | getAll() {
16 | return data.cookies
17 | }
18 | }
19 | })
20 |
21 | const session = isBrowser() ? await getValidatedSession(supabase) : data.session
22 |
23 | return { supabase, session }
24 | }
25 |
--------------------------------------------------------------------------------
/src/routes/(authenticated)/+layout.server.ts:
--------------------------------------------------------------------------------
1 | export const load = async ({ setHeaders }) => {
2 | /**
3 | * Prevents browsers from caching pages which should be protected in all scenarios.
4 | *
5 | * This is not strictly necessary, but be aware of the following:
6 | * If a user is logged in and uses a feature that utilizes a form action,
7 | * when that form action returns, the browser URL still contains the search param.
8 | * e.g. http://localhost:5173/app?/some_form_action
9 | *
10 | * If the user then logs out and clicks/taps the browser back navigation button,
11 | * some browsers will serve the previous page via cache, potentially exposing
12 | * sensitive data. I understand this example is a bit contrived, since
13 | * the user just saw the data before logging out.
14 | *
15 | * If you aren't concerned about the above, I'd still recommend
16 | * setting this to 'private' so sensitive data can't be leaked across users via the cache.
17 | */
18 | setHeaders({
19 | 'cache-control': 'no-store'
20 | })
21 | }
--------------------------------------------------------------------------------
/src/routes/auth/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
11 |
14 |
18 |
22 |
25 |
29 |
30 | {#if form?.message}
31 |
{form.message}
32 | {/if}
33 | {#if form?.error}
34 |
{form.error}
35 | {/if}
36 | {#if form?.verify}
37 |
42 | {/if}
43 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'
2 | import { createServerClient } from '@supabase/ssr'
3 | import { redirect } from '@sveltejs/kit'
4 | import type { Session } from '@supabase/supabase-js'
5 | import { getValidatedSession } from '$lib/utils.js'
6 |
7 | export const handle = async ({ event, resolve }) => {
8 | event.locals.supabase = createServerClient(
9 | PUBLIC_SUPABASE_URL,
10 | PUBLIC_SUPABASE_PUBLISHABLE_KEY,
11 | {
12 | cookies: {
13 | getAll: () => event.cookies.getAll(),
14 | setAll: (cookies) => {
15 | cookies.forEach(({ name, value, options }) => {
16 | event.cookies.set(name, value, { ...options, path: '/' })
17 | })
18 | }
19 | }
20 | }
21 | )
22 |
23 | /**
24 | * We use getSession, as a function, rather than a static object
25 | * like `session`, in order to make reactivity work for some
26 | * features of our pages. For example, if this wasn't a function,
27 | * things like the `update_nickname` form action in /self
28 | * wouldn't correctly update data on its page.
29 | */
30 | event.locals.getSession = async (): Promise => {
31 | return await getValidatedSession(event.locals.supabase)
32 | }
33 |
34 | const session = await event.locals.getSession()
35 |
36 | /**
37 | * Only authenticated users can access these paths and their sub-paths.
38 | *
39 | * If you'd rather do this in your routes, see (authenticated)/app/+page.server.ts
40 | * for an example.
41 | *
42 | * If you don't use a layout group for auth-protected paths, then you can use
43 | * new Set(['app', 'self']) or whatever your top-level path segments are, and
44 | * .has(event.url.pathname.split('/')[1])
45 | */
46 | const auth_protected_paths = new Set(['(authenticated)'])
47 | if (!session && auth_protected_paths.has(event.route.id?.split('/')[1] || ''))
48 | redirect(307, '/auth')
49 |
50 | return resolve(event, {
51 | filterSerializedResponseHeaders(name) {
52 | return name === 'content-range' || name === 'x-supabase-api-version'
53 | },
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
48 |
49 |
73 |
74 | {@render children?.()}
75 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { SupabaseClient, Session } from "@supabase/supabase-js"
2 |
3 | /**
4 | * Validate a session on the client or server side.
5 | */
6 | export const getValidatedSession = async (supabase: SupabaseClient): Promise => {
7 | const session = (await supabase.auth.getSession()).data.session
8 |
9 | if (!session) return null
10 |
11 | /* We wrap getClaims in a try/catch, because it could throw. */
12 | try {
13 | /**
14 | * If your project is using symmetric JWTs,
15 | * getClaims makes a network call to your Supabase instance.
16 | * If using asymmetric JWTs, most network calls will hit
17 | * Supabase's global cache, returning within a few milliseconds.
18 | *
19 | * We pass the access_token into getClaims, otherwise it
20 | * would call getSession itself - which we've already done above.
21 | *
22 | * If you need data that is only returned from `getUser`,
23 | * then you can substitute it here and assign accordingly in the return statement.
24 | *
25 | * getClaims does not check the following about the user.
26 | * If you need these, use getUser.
27 | * - is deleted
28 | * - is banned
29 | * - has been logged out globally from a different client
30 | * - JWT's session id is listed in auth.sessions
31 | * - (etc)
32 | */
33 | const { data, error } = await supabase.auth.getClaims(session.access_token)
34 |
35 | if (error || !data) return null
36 |
37 | const { claims } = data
38 |
39 | /**
40 | * Return a Session, created from validated claims.
41 | *
42 | * For security, the only items you should use from `session` are the access and refresh tokens.
43 | *
44 | * Most of these properties are required for functionality or typing.
45 | * Add any data needed for your layouts or pages.
46 | *
47 | * Here are the properties which aren't required, but we use them in the demo:
48 | * `user.user_metadata.avatar_url`
49 | * `user.user_metadata.nickname`
50 | * `user.email`
51 | * `user.phone`
52 | */
53 | return {
54 | access_token: session.access_token,
55 | refresh_token: session.refresh_token,
56 | expires_at: claims.exp,
57 | expires_in: claims.exp - Math.round(Date.now() / 1000),
58 | token_type: 'bearer',
59 | user: {
60 | app_metadata: claims.app_metadata ?? {},
61 | aud: 'authenticated',
62 | created_at: '', // only found in session.user or getUser
63 | id: claims.sub,
64 | email: claims.email,
65 | phone: claims.phone,
66 | user_metadata: claims.user_metadata ?? {},
67 | is_anonymous: claims.is_anonymous
68 | }
69 | }
70 | } catch (err) {
71 | console.error(err)
72 | return null
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/routes/(authenticated)/self/+page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | {#if session}
16 |