teta.so

SvelteKit Authentication: The Complete Guide

Learn how to implement authentication in SvelteKit with Supabase, including protected routes, session management, and social login.

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.

Frequently Asked Questions

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.

Ready to start building?

Create your next web app with AI-powered development tools.

Start Building Free
← All articles