If you’re building a SaaS product with SvelteKit, you’ll need auth — and you’ll need it to be more than just a login form. You need email verification, password reset, profile management, and subscription gating. Rolling all of that yourself is weeks of work. Outseta gives you those pieces as a drop-in service, leaving you free to build the actual product.
This guide walks through the three steps that matter most: adding the Outseta script, protecting server-side routes with JWT verification, and triggering auth flows from your Svelte components.
The Magic Script Setup
Outseta provides a single JavaScript snippet — the “Magic Script” — that bootstraps their client-side SDK. Add it to your src/app.html in the <head>, before SvelteKit hydrates the page:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Outseta Magic Script -->
<script src="https://cdn.outseta.com/outseta.min.js"
data-options='{"domain":"your-domain.outseta.com"}'></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>Replace your-domain.outseta.com with your actual Outseta subdomain from the Outseta dashboard.
Why must it go in <head> before %sveltekit.head%? Because Outseta initialises window.Outseta synchronously when the script runs. If SvelteKit’s head scripts execute first, any component that calls window.Outseta.auth.open() on mount will find window.Outseta undefined. Loading the script first guarantees the SDK is available from the moment your app hydrates.
Protecting Routes with JWT Verification
Outseta issues a JWT access token stored in a cookie named access-token. In SvelteKit, you can verify this token in src/hooks.server.ts and attach the decoded user to event.locals — making the user available to every +page.server.ts and +layout.server.ts down the tree.
Here’s a minimal hooks.server.ts using the Outseta REST API to verify the token (no asymmetric key management required):
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('access-token');
if (token) {
try {
// Verify the token against Outseta's public endpoint.
// This is a lightweight call — Outseta validates the signature and returns claims.
const res = await fetch('https://your-domain.outseta.com/api/v1/profile', {
headers: { Authorization: `bearer ${token}` }
});
if (res.ok) {
const profile = await res.json();
event.locals.user = {
uid: profile.Uid,
email: profile.Email,
firstName: profile.FirstName,
lastName: profile.LastName,
accountUid: profile.Account?.Uid ?? null,
planUid: profile.Account?.CurrentSubscription?.Plan?.Uid ?? null
};
}
} catch {
// Invalid or expired token — treat as unauthenticated
}
}
return resolve(event);
};You’ll also want to declare the user shape on App.Locals in src/app.d.ts:
declare global {
namespace App {
interface Locals {
user?: {
uid: string;
email: string;
firstName: string;
lastName: string;
accountUid: string | null;
planUid: string | null;
};
}
}
}Once event.locals.user is set, you can guard any route in its +page.server.ts:
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = ({ locals }) => {
if (!locals.user) throw redirect(302, '/login');
return { user: locals.user };
};Triggering Login from SvelteKit
Outseta’s SDK exposes window.Outseta.auth.open() to open the auth widget as a popup — no page redirect required. Here’s a minimal Svelte component that wires up a “Sign in” button:
<script lang="ts">
import { onMount } from 'svelte';
let user = $state<{ email: string } | null>(null);
onMount(() => {
// window.Outseta is available here because the Magic Script loaded first.
const outseta = (window as any).Outseta;
// Read current auth state
const profile = outseta.getUser();
if (profile) user = { email: profile.Email };
// Listen for auth state changes (login, logout, plan change)
outseta.on('accessToken.set', () => {
const updated = outseta.getUser();
user = updated ? { email: updated.Email } : null;
});
});
function openLogin() {
(window as any).Outseta.auth.open({ widgetMode: 'login' });
}
function openSignup() {
(window as any).Outseta.auth.open({ widgetMode: 'register' });
}
function logout() {
(window as any).Outseta.auth.logout();
}
</script>
{#if user}
<span class="text-sm text-gray-600">Signed in as {user.email}</span>
<button onclick={logout} class="btn-secondary">Sign out</button>
{:else}
<button onclick={openLogin} class="btn-secondary">Sign in</button>
<button onclick={openSignup} class="btn-primary">Get started</button>
{/if}The outseta.on('accessToken.set', ...) listener fires whenever the auth state changes — after login, after logout, and after a plan upgrade. This is the right place to sync your local UI state with Outseta’s auth.
Conclusion
Outseta handles the hardest parts of SaaS auth: email verification flows, password reset, account management UI, and billing lifecycle events. By offloading these to Outseta, you avoid maintaining a user management backend entirely.
The three-piece pattern above — Magic Script in app.html, JWT verification in hooks.server.ts, and the auth widget triggered from a Svelte component — gives you a complete, production-ready auth layer with around 60 lines of code. From there you can layer in subscription gating, trial management, and usage tracking using the same event.locals.user.planUid that the hook already provides.
Building something with SvelteKit and Outseta? Get in touch — we’d love to hear what you’re working on.
