Authentication is one of the first things you need to figure out when building a web application, and getting it wrong can have serious consequences — from security vulnerabilities to frustrated users who cannot stay logged in. SvelteKit provides excellent primitives for handling auth: server hooks for middleware-style logic, form actions for login/signup flows, and load functions for protecting routes. This guide walks through implementing authentication in SvelteKit using Supabase Auth, which provides a complete, production-ready auth system with social login, magic links, and row-level security. If you want to skip the setup and start building immediately, Teta comes with Supabase integration built in.
Authentication Options for SvelteKit
Before diving into implementation, it helps to understand the available approaches.
Supabase Auth is a popular choice in the SvelteKit community. It provides email/password authentication, social login (Google, GitHub, Discord, and more), magic links, phone auth, and multi-factor authentication. It runs on top of GoTrue and integrates tightly with Supabase's database and row-level security policies. The @supabase/ssr package handles cookie-based session management specifically designed for server-rendered frameworks like SvelteKit.
Lucia was a widely used auth library that provided a framework-agnostic authentication layer. As of early 2025, Lucia transitioned to a learning resource rather than an actively maintained library. The patterns it established — session tokens stored in cookies, validated on the server — remain the recommended approach for DIY auth.
Auth.js (NextAuth) has a SvelteKit adapter (@auth/sveltekit) that supports OAuth providers, credentials, and magic links. It works but is primarily designed for Next.js, and the SvelteKit integration has historically lagged behind.
Clerk, WorkOS, and Firebase Auth are managed auth services with varying levels of SvelteKit support. They handle the infrastructure but add a vendor dependency and cost.
For most SvelteKit projects, Supabase Auth offers the best balance of features, developer experience, and cost. The rest of this guide focuses on that approach.
Setting Up Supabase Auth in SvelteKit
Start by installing the required packages:
npm install @supabase/supabase-js @supabase/ssr
Create a Supabase client helper that works on both server and client:
// src/lib/supabase.ts
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export function createSupabaseClient(
fetch?: typeof globalThis.fetch,
cookies?: { get: (key: string) => string | undefined }
) {
if (isBrowser()) {
return createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
}
return createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll() {
// Parse cookies from the request
return Object.entries(cookies ?? {}).map(([name, value]) => ({
name,
value: value ?? ''
}));
}
},
global: { fetch }
});
}
The key insight is that Supabase Auth stores session tokens in cookies, so the server can validate authentication on every request without a separate API call.
Protected Routes with Hooks
SvelteKit hooks let you intercept every request before it reaches your routes. This is where you validate the session and protect routes that require authentication.
// src/hooks.server.ts
import { createServerClient } from '@supabase/ssr';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import { redirect, type Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// Create a Supabase client for this request
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: '/' });
});
}
}
}
);
// Get the current session
event.locals.session = await event.locals.supabase.auth.getSession();
// Protect routes under /app
if (event.url.pathname.startsWith('/app')) {
if (!event.locals.session.data.session) {
redirect(303, '/login');
}
}
return resolve(event);
};
Update your app.d.ts to type the locals:
// src/app.d.ts
import type { Session, SupabaseClient } from '@supabase/supabase-js';
declare global {
namespace App {
interface Locals {
supabase: SupabaseClient;
session: { data: { session: Session | null } };
}
}
}
Now every route under /app requires an authenticated session. Unauthenticated users are redirected to /login.
Session Management
Proper session management means handling login, logout, token refresh, and session persistence across browser tabs.
Login with Email and Password
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
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) {
return fail(400, {
error: error.message,
email
});
}
redirect(303, '/app');
},
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) {
return fail(400, {
error: error.message,
email
});
}
return { success: true, message: 'Check your email for a confirmation link.' };
}
};
The corresponding login form uses SvelteKit's progressive enhancement:
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<form method="POST" action="?/login" use:enhance>
<label>
Email
<input type="email" name="email" value={form?.email ?? ''} required />
</label>
<label>
Password
<input type="password" name="password" required />
</label>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<button type="submit">Log in</button>
</form>
<form method="POST" action="?/signup" use:enhance>
<!-- Same fields, different action -->
<button type="submit">Sign up</button>
</form>
Logout
// src/routes/logout/+page.server.ts
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ locals: { supabase } }) => {
await supabase.auth.signOut();
redirect(303, '/');
}
};
Token Refresh
Supabase handles token refresh automatically when using @supabase/ssr. The session cookie is updated with new tokens when the access token expires. In your layout, you can listen for auth state changes to keep the client in sync:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { invalidateAll } from '$app/navigation';
let { data, children } = $props();
onMount(() => {
const { data: { subscription } } = data.supabase.auth.onAuthStateChange(
(event) => {
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
invalidateAll();
}
}
);
return () => subscription.unsubscribe();
});
</script>
{@render children()}
Social Login with Google and GitHub
Supabase makes social login straightforward. First, configure the providers in your Supabase dashboard (Authentication > Providers). Then create a callback route and trigger the OAuth flow.
OAuth Initiation
// src/routes/login/+page.server.ts (add to existing actions)
export const actions = {
// ... existing login/signup actions
google: async ({ locals: { supabase }, url }) => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${url.origin}/auth/callback`
}
});
if (error) return fail(400, { error: error.message });
if (data.url) redirect(303, data.url);
},
github: async ({ locals: { supabase }, url }) => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${url.origin}/auth/callback`
}
});
if (error) return fail(400, { error: error.message });
if (data.url) redirect(303, data.url);
}
};
OAuth Callback
// src/routes/auth/callback/+server.ts
import { redirect } from '@sveltejs/kit';
export async function GET({ url, locals: { supabase } }) {
const code = url.searchParams.get('code');
if (code) {
await supabase.auth.exchangeCodeForSession(code);
}
redirect(303, '/app');
}
Add social login buttons to your login page:
<form method="POST" action="?/google" use:enhance>
<button type="submit">Continue with Google</button>
</form>
<form method="POST" action="?/github" use:enhance>
<button type="submit">Continue with GitHub</button>
</form>
Authentication Best Practices
Getting authentication working is one thing. Getting it right requires attention to several details.
Always Validate on the Server
Never rely on client-side auth checks alone. The client can be tampered with. Use hooks.server.ts and +page.server.ts load functions to verify the session before returning sensitive data:
// src/routes/app/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
export async function load({ locals: { supabase, session } }) {
if (!session.data.session) {
redirect(303, '/login');
}
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', session.data.session.user.id)
.single();
return { profile };
}
Use Row-Level Security
Supabase's RLS policies ensure that even if there is a bug in your application code, users can only access their own data at the database level:
-- Users can only read their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
-- Users can only update their own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
Handle Edge Cases
Consider what happens when a session expires mid-use, when a user opens multiple tabs, or when they navigate directly to a protected URL. Your auth flow should handle these gracefully rather than showing a blank page or an unhandled error.
Rate Limit Auth Endpoints
If you are handling auth yourself (not through Supabase), implement rate limiting on login and signup endpoints to prevent brute-force attacks. Supabase handles this for you at the infrastructure level.
Building authentication from scratch is complex and error-prone. Using a proven solution like Supabase Auth with Teta gives you a production-ready auth system integrated with your SvelteKit project from the start, so you can focus on building your actual product.
FAQ
What is the best auth solution for SvelteKit?
Supabase Auth is the most popular and well-integrated auth solution for SvelteKit in 2026. It provides email/password, social login, magic links, and multi-factor authentication out of the box, with the @supabase/ssr package handling cookie-based sessions specifically designed for server-rendered frameworks.
How do I protect routes in SvelteKit?
You protect routes using hooks.server.ts to intercept requests and check for a valid session before the route loads. For more granular control, use +page.server.ts load functions that redirect unauthenticated users. Always validate the session on the server — client-side checks alone are not sufficient for security.
Can I use JWT with SvelteKit?
Yes, Supabase Auth uses JWTs internally for session management. The access token is a JWT that contains the user's ID and metadata. You can decode it on the server to verify the user's identity. However, most developers do not need to work with JWTs directly — the Supabase client handles token management, refresh, and validation automatically.
How do I add Google login to SvelteKit?
To add Google login, configure Google as an OAuth provider in your Supabase dashboard, then use supabase.auth.signInWithOAuth({ provider: 'google' }) from a SvelteKit form action. Create a callback route at /auth/callback that exchanges the authorization code for a session using supabase.auth.exchangeCodeForSession(code). The entire flow takes about 20 minutes to set up.