← Back to Blog
2025-01-28

SvelteKit Authentication: From Zero to Production

Build production-ready authentication in SvelteKit with this complete guide covering server hooks, form actions, protected routes, social login, and deployment.

SvelteKit Authentication: From Zero to Production

SvelteKit is rapidly becoming one of the most beloved web frameworks out there, and for good reason. It's elegant, performant, and genuinely fun to work with. But when it comes to authentication, the ecosystem is still catching up.

Unlike Next.js—where you'll find authentication guides everywhere—SvelteKit developers often have to piece together auth implementations from scattered resources and outdated Stack Overflow answers.

This changes today. In this comprehensive tutorial, we'll build a complete, production-ready authentication system for SvelteKit. Not just the basics, but everything you need: server hooks, form actions, protected routes, social login, and deployment strategies. By the end, you'll have working authentication that actually feels native to SvelteKit's elegant patterns.

What We're Building

We'll create a full-featured authentication system that includes:

  • Email/password authentication with proper server-side validation
  • Social login (Google and GitHub) with OAuth 2.0
  • Server-side session management using SvelteKit hooks
  • Protected routes at the layout level for clean authorization
  • Reactive client-side auth state with Svelte stores
  • Form actions for auth operations (no client-side JavaScript required)
  • A complete dashboard with logout functionality
  • Production-ready security headers and cookie configuration

All of this will work seamlessly with SvelteKit's mental model—no fighting the framework, just elegant code that follows SvelteKit conventions.

Understanding SvelteKit's Auth Architecture

Before diving into code, let's understand how authentication fits into SvelteKit's architecture. This framework gives us several powerful primitives that make authentication natural:

Server Hooks (hooks.server.ts) run on every request before any route loads, making them perfect for validating sessions and attaching user data to requests.

Load Functions run before rendering pages (on both server and client), ideal for checking auth status and protecting routes without redirects.

Form Actions handle form submissions on the server with progressive enhancement, which is exactly what we need for login and signup flows that work without JavaScript.

Stores let us make auth state reactive on the client side, so our UI updates immediately when users log in or out.

Layouts let us protect entire route trees with a single load function, reducing boilerplate dramatically.

Understanding how these pieces fit together is key. SvelteKit's design makes authentication feel natural once you see the pattern.

SvelteKit Key Concepts for Authentication

ConceptPurposeAuthentication Use
hooks.server.tsServer-side request interceptorValidate session on every request
event.localsRequest-scoped dataStore current user for route access
+page.server.tsServer-only page logicForm actions for login/signup
+layout.server.tsLayout-level server logicProtect entire route sections
Form actionsServer-side form handlingProcess auth without client JS
$page.dataReactive page dataAccess user in components
StoresClient-side reactive stateGlobal auth state management

Project Setup

Let's start by creating a new SvelteKit project from scratch. I'm using TypeScript because it makes auth code much safer and catches errors at build time, but you can adapt this to JavaScript if you prefer.

pnpm create svelte@latest my-auth-app
cd my-auth-app
pnpm install

When prompted:

  • Choose "Skeleton project" (clean slate)
  • Enable TypeScript (recommended for auth)
  • Add ESLint and Prettier (recommended for code quality)

Now install AuthHero's SvelteKit SDK:

pnpm add @productname/sveltekit

Create a .env file in your project root:

PUBLIC_PRODUCTNAME_URL=https://api.productname.com
PRODUCTNAME_API_KEY=your_api_key_here

You'll get your API key from the AuthHero dashboard after creating a project. The PUBLIC_ prefix makes the URL available client-side (needed for some SDK features), while the API key stays server-only for security.

Part 1: Setting Up the Auth Hook

The server hook is the foundation of our auth system. It runs on every request, validates the session token, and attaches user information to event.locals so it's available throughout your app.

Create src/hooks.server.ts:

import { AuthHero } from '@productname/sveltekit';
import type { Handle } from '@sveltejs/kit';
import { PUBLIC_PRODUCTNAME_URL } from '$env/static/public';
import { PRODUCTNAME_API_KEY } from '$env/static/private';

// Initialize AuthHero SDK
const auth = new AuthHero({
  url: PUBLIC_PRODUCTNAME_URL,
  apiKey: PRODUCTNAME_API_KEY,
});

export const handle: Handle = async ({ event, resolve }) => {
  // Get session token from cookies
  const sessionToken = event.cookies.get('session_token');

  if (sessionToken) {
    try {
      // Validate session with AuthHero
      const session = await auth.validateSession(sessionToken);

      if (session.valid) {
        // Attach user to event.locals for access in routes
        event.locals.user = session.user;
        event.locals.sessionId = session.id;
      } else {
        // Session expired or invalid - clear the cookie
        event.cookies.delete('session_token', { path: '/' });
      }
    } catch (error) {
      console.error('Session validation failed:', error);
      // Clear invalid cookie to prevent repeated errors
      event.cookies.delete('session_token', { path: '/' });
    }
  }

  // Continue to route
  return resolve(event);
};

Now update src/app.d.ts to add type definitions for locals. This gives you full TypeScript autocomplete for event.locals.user everywhere:

// See https://kit.svelte.dev/docs/types#app
declare global {
  namespace App {
    interface Locals {
      user?: {
        id: string;
        email: string;
        name?: string;
      };
      sessionId?: string;
    }
    // interface PageData {}
    // interface Error {}
    // interface Platform {}
  }
}

export {};

What's happening here? On every request, we check for a session cookie, validate it with AuthHero, and if valid, attach the user to event.locals. This means everywhere in your SvelteKit app—load functions, form actions, API routes—you'll have typed access to the current user via locals.user.

Performance Note: Session validation happens in-memory on AuthHero's edge network, adding less than 5ms of latency. For even better performance, you can implement a local cache with TTL.

Part 2: Login and Signup Pages

Let's build the login page using SvelteKit's form actions for progressive enhancement. This works without JavaScript, but enhances with client-side validation when available.

Create src/routes/login/+page.svelte:

<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';

  export let form: ActionData;
</script>

<div class="auth-container">
  <h1>Log In</h1>

  <form method="POST" use:enhance>
    {#if form?.error}
      <div class="error" role="alert">
        {form.error}
      </div>
    {/if}

    <div class="form-group">
      <label for="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        value={form?.email ?? ''}
        autocomplete="email"
        required
      />
    </div>

    <div class="form-group">
      <label for="password">Password</label>
      <input
        type="password"
        id="password"
        name="password"
        autocomplete="current-password"
        required
      />
    </div>

    <button type="submit">Log In</button>
  </form>

  <p>
    Don't have an account? <a href="/signup">Sign up</a>
  </p>
</div>

<style>
  .auth-container {
    max-width: 400px;
    margin: 4rem auto;
    padding: 2rem;
  }

  .form-group {
    margin-bottom: 1rem;
  }

  label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
  }

  input {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
  }

  input:focus {
    outline: none;
    border-color: #0066ff;
    box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
  }

  button {
    width: 100%;
    padding: 0.75rem;
    background: #0066ff;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s;
  }

  button:hover {
    background: #0052cc;
  }

  button:active {
    background: #004299;
  }

  .error {
    padding: 0.75rem;
    background: #fee;
    border: 1px solid #fcc;
    border-radius: 4px;
    color: #c00;
    margin-bottom: 1rem;
  }

  p {
    margin-top: 1.5rem;
    text-align: center;
    color: #666;
  }

  a {
    color: #0066ff;
    text-decoration: none;
  }

  a:hover {
    text-decoration: underline;
  }
</style>

Now create the server-side form action in src/routes/login/+page.server.ts:

import { AuthHero } from '@productname/sveltekit';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { PUBLIC_PRODUCTNAME_URL } from '$env/static/public';
import { PRODUCTNAME_API_KEY } from '$env/static/private';

const auth = new AuthHero({
  url: PUBLIC_PRODUCTNAME_URL,
  apiKey: PRODUCTNAME_API_KEY,
});

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email') as string;
    const password = data.get('password') as string;

    // Basic validation
    if (!email || !password) {
      return fail(400, {
        error: 'Email and password are required',
        email,
      });
    }

    // Validate email format
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      return fail(400, {
        error: 'Please enter a valid email address',
        email,
      });
    }

    try {
      // Attempt login with AuthHero
      const session = await auth.signIn({
        email,
        password,
      });

      // Set secure session cookie
      cookies.set('session_token', session.token, {
        path: '/',
        httpOnly: true, // Prevent XSS attacks
        sameSite: 'lax', // CSRF protection
        secure: process.env.NODE_ENV === 'production', // HTTPS only in production
        maxAge: 60 * 60 * 24 * 30, // 30 days
      });

      // Redirect to dashboard
      throw redirect(303, '/dashboard');
    } catch (error: any) {
      console.error('Login error:', error);
      return fail(401, {
        error: 'Invalid email or password',
        email,
      });
    }
  },
};

The signup page is nearly identical. Create src/routes/signup/+page.svelte:

<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';

  export let form: ActionData;
</script>

<div class="auth-container">
  <h1>Sign Up</h1>

  <form method="POST" use:enhance>
    {#if form?.error}
      <div class="error" role="alert">
        {form.error}
      </div>
    {/if}

    <div class="form-group">
      <label for="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        value={form?.email ?? ''}
        autocomplete="email"
        required
      />
    </div>

    <div class="form-group">
      <label for="password">Password</label>
      <input
        type="password"
        id="password"
        name="password"
        minlength="8"
        autocomplete="new-password"
        required
      />
      <small>At least 8 characters</small>
    </div>

    <button type="submit">Sign Up</button>
  </form>

  <p>
    Already have an account? <a href="/login">Log in</a>
  </p>
</div>

<!-- Same styles as login page -->

And src/routes/signup/+page.server.ts:

import { AuthHero } from '@productname/sveltekit';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { PUBLIC_PRODUCTNAME_URL } from '$env/static/public';
import { PRODUCTNAME_API_KEY } from '$env/static/private';

const auth = new AuthHero({
  url: PUBLIC_PRODUCTNAME_URL,
  apiKey: PRODUCTNAME_API_KEY,
});

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email') as string;
    const password = data.get('password') as string;

    // Validation
    if (!email || !password) {
      return fail(400, {
        error: 'Email and password are required',
        email,
      });
    }

    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      return fail(400, {
        error: 'Please enter a valid email address',
        email,
      });
    }

    if (password.length < 8) {
      return fail(400, {
        error: 'Password must be at least 8 characters',
        email,
      });
    }

    try {
      // Create account with AuthHero
      const session = await auth.signUp({
        email,
        password,
      });

      // Set secure session cookie
      cookies.set('session_token', session.token, {
        path: '/',
        httpOnly: true,
        sameSite: 'lax',
        secure: process.env.NODE_ENV === 'production',
        maxAge: 60 * 60 * 24 * 30, // 30 days
      });

      throw redirect(303, '/dashboard');
    } catch (error: any) {
      console.error('Signup error:', error);
      return fail(400, {
        error: error.message || 'Signup failed. Please try again.',
        email,
      });
    }
  },
};

Progressive Enhancement: Notice we're using SvelteKit's form actions—no client-side JavaScript required for basic auth flows. The use:enhance directive provides progressive enhancement for a better UX when JavaScript is available.

Part 3: Protected Routes

Now let's create protected routes that require authentication. We'll protect them at the layout level so any nested routes are also protected automatically.

Create src/routes/dashboard/+layout.server.ts:

import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals, url }) => {
  // If no user is in locals, they're not authenticated
  if (!locals.user) {
    // Preserve the intended destination for post-login redirect
    const redirectTo = url.pathname + url.search;
    throw redirect(303, `/login?redirect=${encodeURIComponent(redirectTo)}`);
  }

  // Pass user to all dashboard pages
  return {
    user: locals.user,
  };
};

Create src/routes/dashboard/+layout.svelte:

<script lang="ts">
  import type { LayoutData } from './$types';

  export let data: LayoutData;
</script>

<div class="dashboard-layout">
  <nav>
    <div class="nav-content">
      <h2>Dashboard</h2>
      <div class="user-info">
        <span>{data.user.email}</span>
        <form method="POST" action="/logout">
          <button type="submit">Log Out</button>
        </form>
      </div>
    </div>
  </nav>

  <main>
    <slot />
  </main>
</div>

<style>
  .dashboard-layout {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }

  nav {
    background: #f8f9fa;
    border-bottom: 1px solid #ddd;
  }

  .nav-content {
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  h2 {
    margin: 0;
    font-size: 1.25rem;
  }

  .user-info {
    display: flex;
    align-items: center;
    gap: 1rem;
  }

  .user-info span {
    color: #666;
    font-size: 0.875rem;
  }

  main {
    flex: 1;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
    width: 100%;
  }

  button {
    padding: 0.5rem 1rem;
    background: #dc3545;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s;
  }

  button:hover {
    background: #c82333;
  }
</style>

Create src/routes/dashboard/+page.svelte:

<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<div class="dashboard-content">
  <h1>Welcome, {data.user.name || data.user.email}!</h1>

  <div class="card">
    <h2>Your Account</h2>
    <dl>
      <dt>User ID:</dt>
      <dd>{data.user.id}</dd>

      <dt>Email:</dt>
      <dd>{data.user.email}</dd>
    </dl>
  </div>

  <p class="help-text">
    This is your protected dashboard. All routes under /dashboard are automatically protected by the layout.
  </p>
</div>

<style>
  .dashboard-content {
    max-width: 800px;
  }

  h1 {
    margin-bottom: 2rem;
  }

  .card {
    background: white;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
  }

  .card h2 {
    margin-top: 0;
    margin-bottom: 1rem;
    font-size: 1.25rem;
  }

  dl {
    margin: 0;
  }

  dt {
    font-weight: 500;
    margin-top: 0.5rem;
    color: #666;
  }

  dd {
    margin: 0.25rem 0 1rem 0;
    color: #333;
  }

  .help-text {
    color: #666;
    font-size: 0.875rem;
  }
</style>

Now create the logout action at src/routes/logout/+server.ts:

import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ cookies }) => {
  // Clear session cookie
  cookies.delete('session_token', { path: '/' });

  // Redirect to home page
  throw redirect(303, '/');
};

Perfect! Now anyone trying to access /dashboard without being logged in will be redirected to /login with their intended destination preserved.

SvelteKit Hooks Flow Diagram

Understanding the request flow helps debug authentication issues:

┌─────────────────────────────────────────────────────────┐
│                   Browser Request                        │
│                  GET /dashboard                          │
└──────────────────┬──────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────┐
│              hooks.server.ts (handle)                    │
│  • Extract session_token cookie                         │
│  • Validate with AuthHero                            │
│  • Attach user to event.locals                          │
└──────────────────┬──────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────┐
│         +layout.server.ts (load function)                │
│  • Check if locals.user exists                          │
│  • If not, redirect to /login                           │
│  • If yes, return user data                             │
└──────────────────┬──────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────┐
│            +page.svelte (component)                      │
│  • Receive user via data prop                           │
│  • Render protected content                             │
│  • Access user in template                              │
└─────────────────────────────────────────────────────────┘

Part 4: Social Authentication

Let's add Google OAuth for social login. First, configure Google OAuth in your AuthHero dashboard and get your redirect URL (usually https://yourdomain.com/auth/callback/google).

Create src/routes/auth/google/+server.ts:

import { AuthHero } from '@productname/sveltekit';
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { PUBLIC_PRODUCTNAME_URL } from '$env/static/public';
import { PRODUCTNAME_API_KEY } from '$env/static/private';

const auth = new AuthHero({
  url: PUBLIC_PRODUCTNAME_URL,
  apiKey: PRODUCTNAME_API_KEY,
});

export const GET: RequestHandler = async ({ url }) => {
  // Get the OAuth authorization URL from AuthHero
  const { url: authUrl } = await auth.getOAuthURL({
    provider: 'google',
    redirectUri: 'https://yourdomain.com/auth/callback/google',
    // Optionally pass state for CSRF protection
    state: url.searchParams.get('redirect') || '/dashboard',
  });

  throw redirect(303, authUrl);
};

Create the callback handler at src/routes/auth/callback/google/+server.ts:

import { AuthHero } from '@productname/sveltekit';
import { redirect, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { PUBLIC_PRODUCTNAME_URL } from '$env/static/public';
import { PRODUCTNAME_API_KEY } from '$env/static/private';

const auth = new AuthHero({
  url: PUBLIC_PRODUCTNAME_URL,
  apiKey: PRODUCTNAME_API_KEY,
});

export const GET: RequestHandler = async ({ url, cookies }) => {
  const code = url.searchParams.get('code');
  const state = url.searchParams.get('state');

  if (!code) {
    throw error(400, 'Missing authorization code');
  }

  try {
    // Exchange code for session token
    const session = await auth.handleOAuthCallback({
      provider: 'google',
      code,
      state,
    });

    // Set secure session cookie
    cookies.set('session_token', session.token, {
      path: '/',
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 24 * 30,
    });

    // Redirect to dashboard or state destination
    const redirectTo = state || '/dashboard';
    throw redirect(303, redirectTo);
  } catch (err) {
    console.error('OAuth callback error:', err);
    throw error(500, 'OAuth authentication failed');
  }
};

Add a "Sign in with Google" button to your login page:

<!-- In src/routes/login/+page.svelte -->
<div class="auth-container">
  <h1>Log In</h1>

  <div class="social-login">
    <a href="/auth/google" class="oauth-button google">
      <svg width="18" height="18" viewBox="0 0 18 18">
        <!-- Google icon SVG -->
      </svg>
      Continue with Google
    </a>
  </div>

  <div class="divider">
    <span>or</span>
  </div>

  <form method="POST" use:enhance>
    <!-- Email/password form -->
  </form>
</div>

GitHub OAuth follows the exact same pattern—just create /auth/github and /auth/callback/github endpoints.

Part 5: Client-Side Reactive State

For a more dynamic experience, let's create a store that holds auth state and makes it reactive across your entire app. Create src/lib/stores/auth.ts:

import { writable } from 'svelte/store';
import { browser } from '$app/environment';

interface User {
  id: string;
  email: string;
  name?: string;
}

function createAuthStore() {
  const { subscribe, set, update } = writable<User | null>(null);

  return {
    subscribe,
    setUser: (user: User | null) => set(user),
    logout: async () => {
      if (browser) {
        await fetch('/logout', { method: 'POST' });
        set(null);
        window.location.href = '/';
      }
    },
  };
}

export const auth = createAuthStore();

Update your root layout to initialize the store. Create src/routes/+layout.svelte:

<script lang="ts">
  import { auth } from '$lib/stores/auth';
  import type { LayoutData } from './$types';

  export let data: LayoutData;

  // Reactively sync user data to store
  $: auth.setUser(data.user ?? null);
</script>

<slot />

And create src/routes/+layout.server.ts:

import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    user: locals.user ?? null,
  };
};

Now you can use the auth store anywhere in your app:

<script lang="ts">
  import { auth } from '$lib/stores/auth';
</script>

{#if $auth}
  <p>Logged in as {$auth.email}</p>
  <button on:click={() => auth.logout()}>
    Log out
  </button>
{:else}
  <a href="/login">Log in</a>
{/if}

Production Deployment Checklist

Before deploying your SvelteKit authentication to production, verify these essential security and functionality requirements:

Security Configuration

  • [ ] Secure Cookies: httpOnly: true, sameSite: 'lax', secure: true in production
  • [ ] Environment Variables: API keys in environment variables, never committed to git
  • [ ] CSRF Protection: SvelteKit's form actions include CSRF protection (don't disable)
  • [ ] HTTPS: Always use HTTPS in production (platform usually handles this)
  • [ ] Rate Limiting: Consider adding rate limiting to login/signup endpoints

Authentication Flow

  • [ ] Login works: Email/password authentication functional
  • [ ] Signup works: New user registration functional
  • [ ] Logout works: Session cleared and redirects correctly
  • [ ] Protected routes: Unauthenticated users redirected to login
  • [ ] OAuth works: Social login functional (if implemented)
  • [ ] Session persistence: Users stay logged in across browser restarts

Error Handling

  • [ ] Invalid credentials: Clear error messages displayed
  • [ ] Network errors: Graceful degradation when API unavailable
  • [ ] Session expiry: Expired sessions handled gracefully
  • [ ] Validation errors: Form validation messages shown to users

User Experience

  • [ ] Loading states: Forms show loading during submission
  • [ ] Success feedback: Users see confirmation after actions
  • [ ] Redirect preservation: Return to intended page after login
  • [ ] Browser autocomplete: Email and password fields have proper autocomplete attributes

Deployment Platform Configuration

SvelteKit apps with authentication deploy beautifully to multiple platforms:

Vercel Deployment

Vercel auto-detects SvelteKit and configures everything:

# Install Vercel adapter
pnpm add -D @sveltejs/adapter-vercel

# Update svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter()
  }
};

Environment variables: Add in Vercel dashboard under Settings → Environment Variables.

Cloudflare Pages Deployment

Excellent for edge performance:

# Install Cloudflare adapter
pnpm add -D @sveltejs/adapter-cloudflare

# Update svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter()
  }
};

Build settings:

  • Build command: pnpm build
  • Output directory: .svelte-kit/cloudflare

Node.js Server Deployment

For traditional hosting or Docker:

# Install Node adapter
pnpm add -D @sveltejs/adapter-node

# Update svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter()
  }
};

Build and run:

pnpm build
node build

Deployment Platform Comparison

PlatformSetup ComplexityEdge SupportCost (Hobby)
Vercel⭐⭐⭐⭐⭐ One-click✅ YesFree tier generous
Cloudflare Pages⭐⭐⭐⭐ Easy✅ Yes (excellent)Free tier very generous
Netlify⭐⭐⭐⭐ Easy✅ YesFree tier good
Railway⭐⭐⭐ Moderate❌ No$5/month
Fly.io⭐⭐⭐ Moderate✅ Multi-regionPay-as-you-go

Troubleshooting Common Issues

Hydration Mismatches

Problem: Console shows hydration errors when displaying user data.

Solution: Make sure you're not using the $auth store during SSR. Wrap client-only code in {#if browser} blocks from $app/environment:

<script>
  import { browser } from '$app/environment';
  import { auth } from '$lib/stores/auth';
</script>

{#if browser}
  <!-- Client-only code using $auth -->
  {#if $auth}
    <p>Logged in as {$auth.email}</p>
  {/if}
{/if}

Cookie Not Setting

Problem: Session cookie isn't being set or read.

Solution: Check that your sameSite and secure settings match your environment:

// Development (HTTP)
cookies.set('session_token', token, {
  httpOnly: true,
  sameSite: 'lax',
  secure: false, // Must be false for HTTP
  path: '/',
  maxAge: 60 * 60 * 24 * 30
});

// Production (HTTPS)
cookies.set('session_token', token, {
  httpOnly: true,
  sameSite: 'lax',
  secure: true, // Must be true for HTTPS
  path: '/',
  maxAge: 60 * 60 * 24 * 30
});

OAuth Redirect Issues

Problem: OAuth flows fail with redirect URI mismatch errors.

Solution: Double-check that your redirect URIs match exactly in both the OAuth provider dashboard and your AuthHero configuration:

// Must match exactly in:
// 1. Google Cloud Console
// 2. AuthHero dashboard
// 3. Your code
redirectUri: 'https://yourdomain.com/auth/callback/google'

Include the protocol (https://) and ensure there are no trailing slashes unless expected.

Session Not Persisting

Problem: Users get logged out between page navigations.

Solution: Verify that your cookie has an appropriate maxAge and that you're not accidentally deleting it:

// ✅ Good - explicit maxAge
cookies.set('session_token', token, {
  path: '/',
  maxAge: 60 * 60 * 24 * 30 // 30 days
});

// ❌ Bad - no maxAge (session cookie)
cookies.set('session_token', token, {
  path: '/'
});

Form Actions Not Working

Problem: Form submissions don't trigger actions or redirect.

Solution: Ensure your form has method="POST" and your action is exported as a named export matching the HTTP method:

<!-- ✅ Correct -->
<form method="POST" use:enhance>
  <!-- form fields -->
</form>
// ✅ Correct
export const actions: Actions = {
  default: async ({ request, cookies }) => {
    // handler
  }
};

Complete Example Repository

I've put together a complete working example with everything in this tutorial, plus automated tests and multiple OAuth providers:

[github.com/productname/sveltekit-auth-example](https://github.com/productname/sveltekit-auth-example)

The repo includes:

  • One-click deploy buttons for Vercel and Cloudflare Pages
  • Playwright tests for all auth flows
  • Additional OAuth providers (GitHub, Discord, Twitter)
  • Email verification flow
  • Password reset functionality
  • Profile management page

What We've Built

In this tutorial, we've created a complete authentication system that:

  • ✅ Validates sessions on every request using server hooks
  • ✅ Handles login and signup with SvelteKit form actions
  • ✅ Protects routes at the layout level for clean authorization
  • ✅ Provides reactive auth state with Svelte stores
  • ✅ Supports OAuth with Google and GitHub
  • ✅ Follows security best practices for production
  • ✅ Deploys to major platforms (Vercel, Cloudflare, Node.js)
  • ✅ Uses TypeScript for type safety throughout

The best part? This implementation feels native to SvelteKit. We're not fighting the framework—we're working with its strengths and conventions.

Next Steps

From here, you might want to add these advanced features:

  • Email verification flows to confirm user email addresses [Link: /docs/email-verification]
  • Password reset functionality for account recovery [Link: /docs/password-reset]
  • Multi-factor authentication for enhanced security [Link: /docs/mfa]
  • Role-based access control for different user types [Link: /docs/rbac]
  • User profile management with custom fields [Link: /docs/profiles]
  • Session management to view and revoke active sessions [Link: /docs/sessions]

AuthHero makes all of these straightforward to add. Check out our SvelteKit documentation for guides on these advanced features.


Ready to implement authentication in your SvelteKit app? AuthHero's free tier includes everything you need to get started—email/password auth, social login, and session management for up to 5,000 monthly active users. Start building with our free tier →