How to use Own Auth with SvelteKit
Own Auth fits naturally into SvelteKit because SvelteKit already provides server-only modules, form actions, request hooks, cookies, and protected server load functions. Own Auth handles users, password hashing, one-time tokens, database sessions, rate limits, and audit events in Postgres.
This guide builds email and password sign-in with a Secure HttpOnly cookie, loads the current session into event.locals, protects routes and mutations, adds magic links, and revokes the session on sign-out. The examples use SvelteKit with TypeScript and server-side rendering.
What you will build
- A server-only Own Auth instance
- Sign-up and sign-in form actions
- An opaque session token stored in an HttpOnly cookie
- A server hook that validates the session for each request
- Typed authentication data in event.locals
- Protected server loads and mutations
- Magic-link request and verification routes
- Sign-out with immediate session revocation
How Own Auth fits into SvelteKit
Browser form
-> SvelteKit form action
-> own-auth
-> Postgres
The browser stores one opaque HttpOnly session token.
The server hook validates it and adds the result to event.locals.
Protected loads and actions still enforce their own authorization.Install and migrate
Install Own Auth and a schema validator, then apply the Own Auth migrations to your Postgres database.
npm install own-auth zod
npx own-auth migrate
npx own-auth statusOwn Auth manages only its own auth tables. If the SvelteKit application also uses Prisma, Drizzle, or another migration tool, keep each tool responsible for its own tables.
Configure private environment values
DATABASE_URL=postgres://user:password@localhost:5432/myapp
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secret
APP_URL=http://localhost:5173Generate the token pepper once, store it with production secrets, and keep it stable. Changing it invalidates existing sessions, authentication links, SMS codes, and API keys.
Create the Own Auth server module
A file under $lib/server cannot be imported into browser code. Read the private values through SvelteKit's environment module and create one shared Own Auth instance. Own Auth reads DATABASE_URL from the server environment.
import { APP_URL, OWN_AUTH_TOKEN_PEPPER } from '$env/static/private';
import { createOwnAuth } from 'own-auth';
if (!APP_URL) throw new Error('APP_URL is required');
if (!OWN_AUTH_TOKEN_PEPPER) {
throw new Error('OWN_AUTH_TOKEN_PEPPER is required');
}
export const auth = createOwnAuth({
tokenPepper: OWN_AUTH_TOKEN_PEPPER,
baseUrl: APP_URL,
});Centralize the session cookie
Own Auth returns an opaque session token and stores only its protected hash in Postgres. Put the raw token in a Secure HttpOnly cookie so browser JavaScript cannot read it.
import { dev } from '$app/environment';
import type { Cookies } from '@sveltejs/kit';
export const SESSION_COOKIE = 'own_auth_session';
const cookieOptions = {
path: '/',
httpOnly: true,
sameSite: 'lax' as const,
secure: !dev,
};
export function setSessionCookie(
cookies: Cookies,
token: string,
expires: Date,
) {
cookies.set(SESSION_COOKIE, token, {
...cookieOptions,
expires,
});
}
export function deleteSessionCookie(cookies: Cookies) {
cookies.delete(SESSION_COOKIE, { path: '/' });
}Use Secure cookies in production and keep the path consistent when setting and deleting them. SameSite=Lax is a practical default for a first-party SvelteKit application while still allowing top-level navigation from an authentication email.
Load the session in a server hook
The handle hook runs for server requests. Read the cookie, verify the database-backed session with Own Auth, and place the result in event.locals. Delete an expired or revoked cookie so the browser stops sending it.
import type { Handle } from '@sveltejs/kit';
import { auth } from '$lib/server/auth';
import {
deleteSessionCookie,
SESSION_COOKIE,
} from '$lib/server/session-cookie';
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get(SESSION_COOKIE);
event.locals.currentAuth = token
? await auth.getCurrentSession(token)
: null;
if (token && !event.locals.currentAuth) {
deleteSessionCookie(event.cookies);
}
return resolve(event);
};Declare the locals type once so server loads, actions, and endpoints all receive the same current-session shape.
import type { auth } from '$lib/server/auth';
declare global {
namespace App {
interface Locals {
currentAuth: Awaited<ReturnType<typeof auth.getCurrentSession>>;
}
}
}
export {};Create the sign-in form action
SvelteKit form actions keep credentials on the server boundary and work without client-side JavaScript. Validate the form before calling Own Auth, set the session cookie, then redirect after the POST.
import { fail, redirect } from '@sveltejs/kit';
import { AuthError } from 'own-auth';
import { z } from 'zod';
import { auth } from '$lib/server/auth';
import { setSessionCookie } from '$lib/server/session-cookie';
import type { Actions } from './$types';
const schema = z.object({
email: z.string().trim().email(),
password: z.string().min(1),
});
export const actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return fail(400, { message: 'Enter a valid email and password.' });
}
let result;
try {
result = await auth.signInEmailPassword(parsed.data);
} catch (error) {
if (error instanceof AuthError) {
return fail(error.statusCode, {
message: error.safeMessage,
email: parsed.data.email,
});
}
throw error;
}
setSessionCookie(
cookies,
result.sessionToken,
result.session.expiresAt,
);
redirect(303, '/dashboard');
},
} satisfies Actions;Wrong emails and wrong passwords both produce Own Auth's generic invalid-credentials error. Preserve that ambiguity so the form does not reveal which accounts exist.
Build the progressively enhanced form
A normal POST form works before JavaScript loads. use:enhance upgrades the interaction without replacing the server action or creating a second authentication path.
<script lang="ts">
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<form method="POST" use:enhance>
<label>
Email
<input
name="email"
type="email"
autocomplete="email"
value={form?.email ?? ''}
required
/>
</label>
<label>
Password
<input
name="password"
type="password"
autocomplete="current-password"
required
/>
</label>
{#if form?.message}
<p role="alert">{form.message}</p>
{/if}
<button type="submit">Sign in</button>
</form>Add sign-up
The sign-up action follows the same pattern. Validate the name, email, and password, call signUpEmailPassword, set the returned session cookie, and redirect. Sign-up may report that an email already exists because the user must resolve that conflict.
const result = await auth.signUpEmailPassword({
name: parsed.data.name,
email: parsed.data.email,
password: parsed.data.password,
});
setSessionCookie(
cookies,
result.sessionToken,
result.session.expiresAt,
);
redirect(303, '/dashboard');Protect server loads and mutations
Use a server load to redirect unauthenticated page requests. Return only the user fields the browser needs. The session token must stay in the HttpOnly cookie and must not be serialized into page data.
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = ({ locals }) => {
if (!locals.currentAuth) redirect(307, '/signin');
return {
user: locals.currentAuth.user,
};
};Sign out and revoke the session
Revoke the database session before deleting the browser cookie. Use a form action because sign-out changes server state and should not be a GET request.
import { redirect } from '@sveltejs/kit';
import { auth } from '$lib/server/auth';
import {
deleteSessionCookie,
SESSION_COOKIE,
} from '$lib/server/session-cookie';
import type { Actions } from './$types';
export const actions = {
default: async ({ cookies }) => {
const token = cookies.get(SESSION_COOKIE);
try {
if (token) await auth.signOut(token);
} finally {
deleteSessionCookie(cookies);
}
redirect(303, '/signin');
},
} satisfies Actions;Add magic-link sign-in
Configure an Own Auth email provider, then add one action to request the email and one server route to consume the one-time token. Always return the same request confirmation regardless of whether an account exists.
await auth.requestMagicLink({
email: parsed.data.email,
});
return {
message: 'If that address can sign in, a link is on its way.',
};import { redirect } from '@sveltejs/kit';
import { AuthError } from 'own-auth';
import { auth } from '$lib/server/auth';
import { setSessionCookie } from '$lib/server/session-cookie';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, cookies }) => {
const token = url.searchParams.get('token');
if (!token) redirect(303, '/signin?error=invalid_link');
let result;
try {
result = await auth.verifyMagicLink({ token });
} catch (error) {
if (error instanceof AuthError) {
redirect(303, '/signin?error=invalid_link');
}
throw error;
}
setSessionCookie(
cookies,
result.sessionToken,
result.session.expiresAt,
);
redirect(303, '/dashboard');
};Production checklist
- Use an SSR-capable SvelteKit adapter and HTTPS in production
- Keep the Own Auth instance and all private environment values in server-only modules
- Store the opaque session token only in a Secure HttpOnly cookie
- Verify the database session in the server hook and each protected mutation
- Use the verified Own Auth user ID for ownership and authorization checks
- Use POST form actions for sign-in, sign-up, and sign-out
- Return generic errors for invalid credentials and magic-link requests
- Never serialize or log passwords, session tokens, provider credentials, or magic-link URLs
- Do not prerender authenticated routes or pages with form actions
FAQ
Can I use Own Auth in a client-only Svelte app?
Not directly. Own Auth needs a trusted server with Postgres access and a private token pepper. A client-only Svelte app must call a separate backend that runs Own Auth. SvelteKit is convenient because it provides that server boundary in the same project.
Should I store the session in a Svelte store?
No. Keep the raw session token in an HttpOnly cookie. A Svelte store may hold non-secret user data returned from a server load, but browser JavaScript should never receive the token.
Is checking the session in hooks.server.ts enough?
It authenticates the request and makes the result available through event.locals. Each protected load, action, and endpoint must still require that result and enforce the user's authorization for the requested resource.
Conclusion
The clean SvelteKit integration keeps responsibilities explicit. Own Auth owns authentication records and revocable database sessions. SvelteKit owns forms, server hooks, cookies, routing, rendering, and authorization around application data. That separation keeps secrets off the client and avoids duplicating auth logic in the framework layer.