--- title: 'Build a User Management App with SvelteKit' description: 'Learn how to use Supabase in your SvelteKit App.' --- <$Partial path="quickstart_intro.mdx" /> ![Supabase User Management example](/docs/img/user-management-demo.png) If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/sveltekit-user-management). <$Partial path="project_setup.mdx" /> ## Building the app Let's start building the Svelte app from scratch. ### Initialize a Svelte app We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript): ```bash npm create svelte@latest supabase-sveltekit cd supabase-sveltekit npm install ``` Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js) ```bash npm install @supabase/supabase-js ``` And finally we want to save the environment variables in a `.env`. All we need are the `SUPABASE_URL` and the `SUPABASE_KEY` key that you copied [earlier](#get-the-api-keys). <$CodeTabs> ```bash name=.env PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL" PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY" ``` Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css). ### Creating a Supabase client for SSR The `ssr` package configures Supabase to use Cookies, which is required for server-side languages and frameworks. Install the Supabase packages: ```bash npm install @supabase/ssr @supabase/supabase-js ``` Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks. Add the code below to your `src/hooks.server.ts` to initialize the client on the server: <$CodeTabs> ```ts name=src/hooks.server.ts // src/hooks.server.ts import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' import { createServerClient } from '@supabase/ssr' import type { Handle } from '@sveltejs/kit' export const handle: Handle = async ({ event, resolve }) => { event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_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) { return name === 'content-range' || name === 'x-supabase-api-version' }, }) } ``` <$Partial path="get_session_warning.mdx" /> If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.safeGetSession`, this can be fixed by updating your `src/app.d.ts` with the content below: <$CodeTabs> ```ts name=src/app.d.ts // src/app.d.ts import { SupabaseClient, Session } from '@supabase/supabase-js' declare global { namespace App { interface Locals { supabase: SupabaseClient safeGetSession(): Promise<{ session: Session | null; user: User | null }> } interface PageData { session: Session | null user: User | null } // interface Error {} // interface Platform {} } } ``` Create a new `src/routes/+layout.server.ts` file to handle the session on the server-side. <$CodeTabs> ```ts name=src/routes/+layout.server.ts // src/routes/+layout.server.ts import type { LayoutServerLoad } from './$types' export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => { const { session, user } = await safeGetSession() return { session, user, cookies: cookies.getAll(), } } ``` Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project. Create a new `src/routes/+layout.ts` file to handle the session and the `supabase` object on the client-side. <$CodeTabs> ```ts name=src/routes/+layout.ts // src/routes/+layout.ts import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' import type { LayoutLoad } from './$types' export const load: LayoutLoad = async ({ fetch, data, depends }) => { depends('supabase:auth') const supabase = isBrowser() ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { fetch, }, }) : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_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() return { supabase, session } } ``` Update your `src/routes/+layout.svelte`: <$CodeTabs> ```svelte name=src/routes/+layout.svelte User Management
``` ## Set up a login page Create a magic link login/signup page for your application: <$CodeTabs> ```svelte name=src/routes/+page.svelte User Management

Supabase + SvelteKit

Sign in via magic link with your email below

{#if form?.message !== undefined}
{form?.message}
{/if}
{#if form?.errors?.email} {form?.errors?.email} {/if}
``` Create a `src/routes/+page.server.ts` file that will handle our magic link form when submitted. <$CodeTabs> ```ts name=src/routes/+page.server.ts // src/routes/+page.server.ts import { fail, redirect } from '@sveltejs/kit' import type { Actions, PageServerLoad } from './$types' export const load: PageServerLoad = async ({ url, locals: { safeGetSession } }) => { const { session } = await safeGetSession() // if the user is already logged in return them to the account page if (session) { redirect(303, '/account') } return { url: url.origin } } export const actions: Actions = { default: async (event) => { const { url, request, locals: { supabase }, } = event const formData = await request.formData() const email = formData.get('email') as string const validEmail = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/.test(email) if (!validEmail) { return fail(400, { errors: { email: 'Please enter a valid email address' }, email }) } const { error } = await supabase.auth.signInWithOtp({ email }) if (error) { return fail(400, { success: false, email, message: `There was an issue, Please contact support.`, }) } return { success: true, message: 'Please check your email for a magic link to log into the website.', } }, } ``` ### Email template Change the email template to support a server-side authentication flow. Before we proceed, let's change the email template to support sending a token hash: - Go to the [Auth templates](/dashboard/project/_/auth/templates) page in your dashboard. - Select `Confirm signup` template. - Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`. - Repeat the previous step for `Magic link` template. Did you know? You could also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates). ### Confirmation endpoint As we are working in a server-side rendering (SSR) environment, it is necessary to create a server endpoint responsible for exchanging the `token_hash` for a session. In the following code snippet, we perform the following steps: - Retrieve the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter. - Exchange this `token_hash` for a session, which we store in storage (in this case, cookies). - Finally, the user is redirected to the `account` page or the `error` page. <$CodeTabs> ```ts name=src/routes/auth/confirm/+server.ts // 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') ?? '/account' /** * 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) } ``` ### Authentication error page If there is an error with confirming the token you will be redirect to this error page. <$CodeTabs> ```svelte name=src/routes/auth/error/+page.svelte

Login error

``` ### Account page After a user is signed in, they need to be able to edit their profile details and manage their account. Create a new `src/routes/account/+page.svelte` file with the content below. <$CodeTabs> ```svelte name=src/routes/account/+page.svelte
``` Now create the associated `src/routes/account/+page.server.ts` file that will handle loading our data from the server through the `load` function and handle all our form actions through the `actions` object. ```ts name=src/routes/account/+page.server.ts import { fail, redirect } from '@sveltejs/kit' import type { Actions, PageServerLoad } from './$types' export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { const { session } = await safeGetSession() if (!session) { redirect(303, '/') } const { data: profile } = await supabase .from('profiles') .select(`username, full_name, website, avatar_url`) .eq('id', session.user.id) .single() return { session, profile } } export const actions: Actions = { update: async ({ request, locals: { supabase, safeGetSession } }) => { const formData = await request.formData() const fullName = formData.get('fullName') as string const username = formData.get('username') as string const website = formData.get('website') as string const avatarUrl = formData.get('avatarUrl') as string const { session } = await safeGetSession() const { error } = await supabase.from('profiles').upsert({ id: session?.user.id, full_name: fullName, username, website, avatar_url: avatarUrl, updated_at: new Date(), }) if (error) { return fail(500, { fullName, username, website, avatarUrl, }) } return { fullName, username, website, avatarUrl, } }, signout: async ({ locals: { supabase, safeGetSession } }) => { const { session } = await safeGetSession() if (session) { await supabase.auth.signOut() redirect(303, '/') } }, } ``` ### Launch! Now that we have all the pages in place, run this in a terminal window: ```bash npm run dev ``` And then open the browser to [localhost:5173](http://localhost:5173) and you should see the completed app. ![Supabase Svelte](/docs/img/supabase-svelte-demo.png) ## Bonus: Profile photos Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory: <$CodeTabs> ```svelte name=src/routes/account/Avatar.svelte
{#if avatarUrl} {avatarUrl {:else}
{/if}
``` ### Add the new widget And then we can add the widget to the Account page: <$CodeTabs> ```svelte name=src/routes/account/+page.svelte
{ profileForm.requestSubmit(); }} />
``` At this stage you have a fully functional application!