Quick Start
This page builds a login in SvelteKit: users in JSON files through @loewen-digital/flatdb, password hashing and server-side sessions from auth, flash messages from session, form validation from validation, all wired by the SvelteKit adapter. Everything runs in one Node process on local files; the Auth on flatdb guide takes the same setup to Cloudflare Workers.
auth stores nothing itself. It takes an AuthDbAdapter, and @loewen-digital/fullstack/auth/flatdb ships one for flatdb collections; on another store you implement the eleven methods yourself.
1. Install
npm install @loewen-digital/fullstack @loewen-digital/flatdb zodPut a secret for the session cookie into .env:
echo "SESSION_SECRET=$(openssl rand -hex 32)" >> .env2. Collections
auth needs three collections: your users with email, passwordHash and emailVerifiedAt, plus sessions and tokens that belong to auth. flatdb strips fields a schema does not declare, so the users schema declares the auth fields; sessions and tokens go without a schema.
// src/lib/server/db.ts
import { flatdb, collection, FsAdapter, type Collection } from '@loewen-digital/flatdb'
import { z } from 'zod'
const userSchema = z.object({
email: z.string().email(),
passwordHash: z.string().nullable().optional(),
emailVerifiedAt: z.string().datetime().nullable().optional(),
})
const db = flatdb(new FsAdapter('./data'), {
users: collection(userSchema),
sessions: collection(),
tokens: collection(),
})
export const users = db.users as Collection<z.infer<typeof userSchema>>
export const sessions = db.sessions as Collection
export const tokens = db.tokens as Collection3. Auth and session
createAuth(config, { db }) takes the adapter; every option has a default. The session module on the cookie driver carries flash messages and old input in a signed cookie.
// src/lib/server/stack.ts
import { env } from '$env/dynamic/private'
import { createAuth } from '@loewen-digital/fullstack/auth'
import { createFlatdbAuthAdapter } from '@loewen-digital/fullstack/auth/flatdb'
import { createSession } from '@loewen-digital/fullstack/session'
import { users, sessions, tokens } from './db'
export const authDb = createFlatdbAuthAdapter({ users, sessions, tokens })
export const auth = createAuth({}, { db: authDb })
export const session = createSession({ driver: 'cookie', secret: env.SESSION_SECRET! })4. The hook
createHandle opens the session from its cookie, validates the auth cookie and fills event.locals on every request.
// src/hooks.server.ts
import { createHandle } from '@loewen-digital/fullstack/adapters/sveltekit'
import { auth, session } from '$lib/server/stack'
export const handle = createHandle({ auth, session })// src/app.d.ts
import type { FullstackLocals } from '@loewen-digital/fullstack/adapters/sveltekit'
import type { SessionHandle } from '@loewen-digital/fullstack/session'
declare global {
namespace App {
interface Locals extends FullstackLocals {
session: SessionHandle
}
}
}
export {}5. Register and log in
validateForm parses the form, validates it against pipe-string rules and, when it fails, flashes the errors and the old input into the session. Registration hashes the password and stores the user; login verifies the hash, opens an auth session and puts its token into the httpOnly auth cookie.
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit'
import { setAuthCookie, validateForm } from '@loewen-digital/fullstack/adapters/sveltekit'
import { auth, authDb } from '$lib/server/stack'
import { users } from '$lib/server/db'
import type { Actions } from './$types'
const rules = { email: 'required|email', password: 'required|string|min:8' }
export const actions: Actions = {
register: async (event) => {
const result = await validateForm(event.request, event.locals.session, rules)
if (!result.ok) return fail(422, { errors: result.errors })
const { email, password } = result.data as { email: string; password: string }
if (await users.findOne({ email })) return fail(422, { error: 'Email already registered' })
await users.insert({ email, passwordHash: await auth.hashPassword(password), emailVerifiedAt: null })
event.locals.session.flash('notice', 'Account created, please log in')
redirect(303, '/login')
},
login: async (event) => {
const result = await validateForm(event.request, event.locals.session, rules)
if (!result.ok) return fail(422, { errors: result.errors })
const { email, password } = result.data as { email: string; password: string }
const user = await authDb.findUserByEmail(email)
if (!user?.passwordHash || !(await auth.verifyPassword(password, user.passwordHash))) {
event.locals.session.flashInput({ email })
return fail(401, { error: 'Invalid credentials' })
}
const authSession = await auth.createSession(user)
setAuthCookie(event, authSession.token)
redirect(303, '/dashboard')
},
}6. Guard a page
locals.authSession is the validated session behind the auth cookie, or null. It carries the user id; the user record is one lookup away.
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit'
import { users } from '$lib/server/db'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.authSession) redirect(303, '/login')
const user = await users.findById(String(locals.authSession.userId))
return { email: user?.email, notice: locals.session.getFlash<string>('notice') }
}7. Log out
// src/routes/logout/+page.server.ts
import { redirect } from '@sveltejs/kit'
import { clearAuthCookie } from '@loewen-digital/fullstack/adapters/sveltekit'
import { auth } from '$lib/server/stack'
import type { Actions } from './$types'
export const actions: Actions = {
default: async (event) => {
if (event.locals.authSession) await auth.destroySession(event.locals.authSession.token)
clearAuthCookie(event)
redirect(303, '/login')
},
}What's next
- Validation for the rules
validateFormunderstands - Auth for email verification, password reset and OAuth
- Session for flash, old input and the cookie driver's limits
- Auth on flatdb for the same setup on Cloudflare Workers