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 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
| Concept | Purpose | Authentication Use |
|---|---|---|
| hooks.server.ts | Server-side request interceptor | Validate session on every request |
| event.locals | Request-scoped data | Store current user for route access |
| +page.server.ts | Server-only page logic | Form actions for login/signup |
| +layout.server.ts | Layout-level server logic | Protect entire route sections |
| Form actions | Server-side form handling | Process auth without client JS |
| $page.data | Reactive page data | Access user in components |
| Stores | Client-side reactive state | Global 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 installWhen 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/sveltekitCreate a .env file in your project root:
PUBLIC_PRODUCTNAME_URL=https://api.productname.com
PRODUCTNAME_API_KEY=your_api_key_hereYou'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:enhancedirective 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: truein 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 buildDeployment Platform Comparison
| Platform | Setup Complexity | Edge Support | Cost (Hobby) |
|---|---|---|---|
| Vercel | ⭐⭐⭐⭐⭐ One-click | ✅ Yes | Free tier generous |
| Cloudflare Pages | ⭐⭐⭐⭐ Easy | ✅ Yes (excellent) | Free tier very generous |
| Netlify | ⭐⭐⭐⭐ Easy | ✅ Yes | Free tier good |
| Railway | ⭐⭐⭐ Moderate | ❌ No | $5/month |
| Fly.io | ⭐⭐⭐ Moderate | ✅ Multi-region | Pay-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 →