SvelteKit Authentication with Supabase Auth
SvelteKit Authentication with Supabase Auth
Authentication is a critical piece of any web application, and getting it right is harder than it looks. You need to handle login flows, session management, token refresh, protected routes, and multiple auth providers — all while keeping the experience fast and secure.
Supabase Auth takes care of the heavy lifting. It provides email/password authentication, OAuth providers (Google, GitHub, and many more), magic links, and phone authentication out of the box. Combined with SvelteKit's server hooks, you can build a robust authentication system in under an hour.
This tutorial walks you through the complete setup, from initial configuration to protecting routes and handling edge cases.
Prerequisites
- A SvelteKit project (see our SvelteKit + Supabase tutorial for setup)
- A Supabase project with the URL and keys ready
@supabase/supabase-jsand@supabase/ssrinstalled
Step 1: Install Dependencies
The @supabase/ssr package provides server-side rendering helpers that handle cookies and session management properly:
npm install @supabase/supabase-js @supabase/ssr
Add your Supabase credentials to .env:
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Step 2: Create the Supabase Client for SSR
The SSR client handles cookies automatically, which is essential for maintaining sessions across server and client. Create src/lib/supabase.ts:
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
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;
}
}
});
const {
data: { session }
} = await supabase.auth.getSession();
const {
data: { user }
} = await supabase.auth.getUser();
return { supabase, session, user };
};
Step 3: Set Up Server Hooks
The server hook is where the magic happens. It creates a Supabase client for every request, manages cookies, and makes the session available throughout your app.
Create src/hooks.server.ts:
import { createServerClient } from '@supabase/ssr';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
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(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, {
...options,
path: '/'
});
});
}
}
}
);
// Convenience helper to get the session
event.locals.safeGetSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
if (!session) {
return { session: null, user: null };
}
// Validate the user with getUser() for security
const {
data: { user },
error
} = await event.locals.supabase.auth.getUser();
if (error) {
return { session: null, user: null };
}
return { session, user };
};
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range' || name === 'x-supabase-api-version';
}
});
};
Update your src/app.d.ts to type the locals:
import type { SupabaseClient, Session, User } 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;
}
}
}
export {};
Step 4: Pass Session Data to Pages
Create a root layout server load function at src/routes/+layout.server.ts:
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals, cookies }) => {
const { session, user } = await locals.safeGetSession();
return {
session,
user,
cookies: cookies.getAll()
};
};
Step 5: Implement Email/Password Authentication
Create a login page at src/routes/login/+page.svelte:
<script lang="ts">
import { goto } from '$app/navigation';
let { data } = $props();
let email = $state('');
let password = $state('');
let loading = $state(false);
let error = $state('');
let mode = $state<'login' | 'signup'>('login');
async function handleSubmit() {
loading = true;
error = '';
const { supabase } = data;
if (mode === 'login') {
const { error: authError } = await supabase.auth.signInWithPassword({
email,
password
});
if (authError) {
error = authError.message;
loading = false;
return;
}
} else {
const { error: authError } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
});
if (authError) {
error = authError.message;
loading = false;
return;
}
error = 'Check your email for a confirmation link.';
loading = false;
return;
}
goto('/dashboard');
}
</script>
<div class="max-w-md mx-auto mt-20 p-6">
<h1 class="text-2xl font-bold mb-6">
{mode === 'login' ? 'Sign In' : 'Create Account'}
</h1>
{#if error}
<div class="p-3 mb-4 bg-red-50 text-red-700 rounded-lg">{error}</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium mb-1">Email</label>
<input
id="email"
type="email"
bind:value={email}
class="w-full p-3 border rounded-lg"
required
/>
</div>
<div>
<label for="password" class="block text-sm font-medium mb-1">Password</label>
<input
id="password"
type="password"
bind:value={password}
class="w-full p-3 border rounded-lg"
minlength="6"
required
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : mode === 'login' ? 'Sign In' : 'Sign Up'}
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-600">
{mode === 'login' ? 'Don''t have an account?' : 'Already have an account?'}
<button
onclick={() => mode = mode === 'login' ? 'signup' : 'login'}
class="text-blue-600 hover:underline"
>
{mode === 'login' ? 'Sign up' : 'Sign in'}
</button>
</p>
</div>
Step 6: Add OAuth Providers (GitHub and Google)
First, configure OAuth providers in your Supabase dashboard under Authentication > Providers. For GitHub, you will need to create an OAuth app at github.com/settings/developers. For Google, set up credentials in the Google Cloud Console.
Add OAuth buttons to your login page:
<script lang="ts">
async function signInWithGitHub() {
const { supabase } = data;
const { error: authError } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (authError) error = authError.message;
}
async function signInWithGoogle() {
const { supabase } = data;
const { error: authError } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
});
if (authError) error = authError.message;
}
</script>
<div class="space-y-3 mb-6">
<button
onclick={signInWithGitHub}
class="w-full py-3 border rounded-lg hover:bg-gray-50 flex items-center justify-center gap-2"
>
Continue with GitHub
</button>
<button
onclick={signInWithGoogle}
class="w-full py-3 border rounded-lg hover:bg-gray-50 flex items-center justify-center gap-2"
>
Continue with Google
</button>
</div>
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white px-4 text-gray-500">or continue with email</span>
</div>
</div>
Step 7: Create the Auth Callback Route
OAuth providers redirect back to your app after authentication. Create the callback handler at src/routes/auth/callback/+server.ts:
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, locals }) => {
const code = url.searchParams.get('code');
const next = url.searchParams.get('next') ?? '/dashboard';
if (code) {
const { error } = await locals.supabase.auth.exchangeCodeForSession(code);
if (!error) {
redirect(303, next);
}
}
// Something went wrong, redirect to error page
redirect(303, '/auth/error');
};
Step 8: Protect Routes with Auth Guards
Create a middleware-style auth guard in your hooks. Update src/hooks.server.ts:
import { redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit';
const protectedRoutes = ['/dashboard', '/settings', '/profile'];
const authGuard: Handle = async ({ event, resolve }) => {
const { session } = await event.locals.safeGetSession();
const isProtectedRoute = protectedRoutes.some(
route => event.url.pathname.startsWith(route)
);
if (isProtectedRoute && !session) {
redirect(303, '/login');
}
// Redirect logged-in users away from login page
if (event.url.pathname === '/login' && session) {
redirect(303, '/dashboard');
}
return resolve(event);
};
export const handle = sequence(supabaseHandle, authGuard);
For page-level protection, you can also check in +page.server.ts:
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
const { session, user } = await locals.safeGetSession();
if (!session) {
redirect(303, '/login');
}
return {
user
};
};
Step 9: Handle Sign Out
Create a sign-out action that works with progressive enhancement:
// src/routes/auth/signout/+server.ts
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ locals }) => {
await locals.supabase.auth.signOut();
redirect(303, '/login');
};
Use it in your navigation:
<script lang="ts">
let { data } = $props();
</script>
{#if data.user}
<div class="flex items-center gap-4">
<span>{data.user.email}</span>
<form method="POST" action="/auth/signout">
<button class="text-red-600 hover:underline">Sign Out</button>
</form>
</div>
{:else}
<a href="/login" class="text-blue-600 hover:underline">Sign In</a>
{/if}
Step 10: Listen for Auth State Changes
Keep your UI in sync with authentication state changes on the client side. Add this to your root layout:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
let { data, children } = $props();
onMount(() => {
const { data: { subscription } } = data.supabase.auth.onAuthStateChange(
(event, session) => {
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
invalidate('supabase:auth');
}
}
);
return () => subscription.unsubscribe();
});
</script>
{@render children()}
This ensures that when a user's session changes (sign in, sign out, or token refresh), the entire page data is reloaded with the new auth state.
Security Best Practices
Always use
getUser()for verification. ThegetSession()method reads from cookies which can be tampered with. UsegetUser()to verify the JWT with Supabase's servers.Set proper cookie options. The
@supabase/ssrpackage handles this, but verify your cookies areHttpOnly,Secure, and useSameSite=Lax.Validate on the server. Never trust client-side auth state for sensitive operations. Always check the session in your
+page.server.tsor+server.tsfiles.Use Row Level Security. Even with auth guards on your routes, RLS ensures data security at the database level. A user should never be able to access data they do not own, even if they bypass your frontend.
Handle token refresh. The
onAuthStateChangelistener withTOKEN_REFRESHEDensures your app stays authenticated even during long sessions.
Summary
You now have a complete authentication system in SvelteKit with Supabase that includes:
- Email/password authentication with sign-up and sign-in
- OAuth providers (GitHub and Google)
- Server-side session management via hooks
- Protected routes with auth guards
- Proper sign-out handling
- Real-time auth state synchronization
- Security best practices
This foundation supports any authentication flow you need. From here, you can add password reset, email verification, multi-factor authentication, or role-based access control — all supported by Supabase Auth.
Source: Teta Engineering
This content was generated with AI from public sources. It represents analysis and commentary, not original journalism.