SvelteKit authentication: what fits in a two-week sprint
A focused SvelteKit authentication flow can fit into a two-week sprint when the database, deployment target and account rules are agreed before the work starts. “Add auth” is not a useful scope by itself. Email/password login, team invitations and enterprise SSO are different jobs.
This guide turns that request into a set of decisions, a small Better Auth integration and acceptance checks. Updated September 14, 2026. The integration slice was checked with SvelteKit 2.70.3, Svelte 5.57.0 and Better Auth 1.7.4. It is not a promise that every authentication project takes two weeks.
Decide who can access what before choosing a library
Write down the first account journey: how someone signs up, proves control of their email, signs in, recovers access and signs out. Then list the data they can read and change. Authentication establishes who is making the request; authorization decides whether that person may perform this particular operation. SvelteKit’s auth guide makes the same distinction.
For an MVP with private saved projects, a useful first rule is: a signed-in person can read and edit only projects they own. That rule needs to hold when the person changes a project ID in the URL or sends a request directly to an endpoint. Hiding an Edit button does not enforce it.
Account methods: email/password, one social provider, or both?
Identity rules: can two providers link to one account, and what proves they belong to the same person?
Access rules: individual ownership, roles, teams, invitations or paid entitlements?
Recovery: who can reset access, what expires, and which sessions should be revoked?
Operations: who owns the email sender, OAuth credentials, deployment secrets and database migrations?
If the answers include several organizations per user, SAML, a migration from an existing user database or custom account linking, scope those explicitly. They may need a separate sprint. Do not hide them under a generic “login” ticket.
Choose a maintained starting point
Better Auth is one current option with a documented SvelteKit integration. It can handle account methods and sessions while your app retains responsibility for resource permissions and deployment. A hosted identity provider can also make sense when you need managed account operations or an existing enterprise identity system. Choose based on the required flow, hosting compatibility and who will maintain it.
The original version of this article recommended Lucia as an installable library. Lucia was deprecated in March 2025 and now provides implementation guidance. Do not start a new production project by installing an old Lucia starter without an explicit maintenance plan. The historical KitForStartups repository is not evidence that its dependency versions are current.
For a conventional server-rendered app, a server-validated session cookie is a straightforward starting point. Keep signing secrets and database credentials in server-only code. Do not treat a decoded JWT, a client-side store or a value read from localStorage as proof that a request is authorized.
A small Better Auth integration to build on
The following files show the server integration, not a complete account product. Start with a SvelteKit TypeScript app and a Better Auth instance configured using its installation guide. Create its database tables before serving requests. For a new project, the Svelte CLI Better Auth add-on is another supported starting point; review the generated configuration rather than pasting a second auth setup on top.
For the isolated local check, src/lib/server/auth.ts used this minimal Node.js SQLite configuration. Install better-auth@1.7.4. Set BETTER_AUTH_SECRET to a high-entropy secret of at least 32 characters and BETTER_AUTH_URL to the app’s exact origin. Keep the secret out of source control. node:sqlite is runtime-specific; use the documented adapter for your production database instead of assuming this file works on an edge worker.
import { betterAuth } from 'better-auth';
import { DatabaseSync } from 'node:sqlite';
const auth = betterAuth({
database: new DatabaseSync('./auth.sqlite'),
emailAndPassword: { enabled: true },
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL
});
export { auth };This local setup enables email/password accounts for integration testing. It does not configure verification or password-reset delivery, a public signup UI or production abuse controls. Those are required scope decisions before launching the corresponding flows.
With the built-in Kysely adapter, Better Auth documents programmatic migrations. The fixture used this one-time setup script at the project root. Apply production migrations through your reviewed deployment workflow, not on every request.
import { getMigrations } from 'better-auth/db/migration';
import { auth } from './src/lib/server/auth.ts';
const { runMigrations } = await getMigrations(auth.options);
await runMigrations();Run the script with a Node version that supports the SQLite driver and TypeScript stripping; the local fixture used Node 26.7.0. See the SQLite adapter documentation for runtime requirements. This test does not establish compatibility with other Node releases or database drivers.
In src/hooks.server.ts, validate the incoming session and populate request-local state. The handler mounts Better Auth’s endpoints; it does not populate your locals automatically.
import { building } from '$app/environment';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { auth } from '$lib/server/auth';
import type { Handle } from '@sveltejs/kit';
const handle: Handle = async ({ event, resolve }) => {
const current = building ? null : await auth.api.getSession({ headers: event.request.headers });
event.locals.user = current?.user ?? null;
event.locals.session = current?.session ?? null;
return svelteKitHandler({ event, resolve, auth, building });
};
export { handle };Add the corresponding types in src/app.d.ts. SvelteKit uses declaration merging here, which is why Locals is an interface rather than a standalone type alias.
import type { auth } from '$lib/server/auth';
type AuthSession = typeof auth.$Infer.Session;
declare global {
namespace App {
interface Locals {
user: AuthSession['user'] | null;
session: AuthSession['session'] | null;
}
}
}
export {};Avoid putting user state in a shared module variable. event.locals belongs to the current request. Return only the fields a page needs, and keep session tokens out of page data.
Protect each server operation
For example, src/routes/api/me/+server.ts exposes only the authenticated person’s ID and name. An anonymous request receives 401. The response is marked private and not stored by caches.
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
const GET: RequestHandler = ({ locals }) => {
if (!locals.user || !locals.session) error(401, 'Sign in to continue.');
return json({ user: { id: locals.user.id, name: locals.user.name } }, {
headers: { 'Cache-Control': 'private, no-store' }
});
};
export { GET };That endpoint proves identity, not general project permissions. An endpoint such as /api/projects/[id] must also check ownership or membership for the requested project. Put that condition in the server-side data query or permission layer, and apply it to writes as well as reads. Test with two separate accounts: account A must not read or modify account B’s project after changing an ID.
A layout redirect can improve navigation, but it is not the only gate for an API, form action or remote function. Check authorization where protected data is read or changed. The same rule applies if you use SvelteKit remote functions instead of route actions.
If you call Better Auth’s sign-in or sign-up API from a SvelteKit server action, its integration guide specifies the sveltekitCookies(getRequestEvent) plugin as the last plugin so cookies reach the response. That is a separate requirement from merely mounting svelteKitHandler. The fixture here calls Better Auth’s HTTP endpoints directly and does not test an action-based login UI.
Treat passwords and recovery as separate work
Passwords are hashed, not encrypted and decrypted for login. Better Auth’s email/password documentation describes its default scrypt hashing and its reset callbacks. Do not write reversible password storage or log submitted credentials.
A login screen is incomplete if people cannot regain access. Decide whether verification is required before sign-in, configure email delivery, and build the reset destination page. Test expired and reused reset links. Better Auth does not revoke other active sessions on password reset by default; set and test revokeSessionsOnPasswordReset when that is the policy you want.
Confirm cookie behavior on the actual HTTPS domain, including any reverse proxy. Keep trusted origins limited to the origins you operate. Do not disable CSRF or origin checks to make a deployment error disappear. The Better Auth configuration reference documents those settings.
Also review rate limiting: the limiter is enabled by default in production, but your storage backend and trusted client-IP configuration must match the deployment. A local development login test does not prove that throttling works across several workers or server instances.
A realistic two-week authentication scope
Here is an example scope to discuss, not a fixed promise for every app: one existing SvelteKit app, one database, email/password accounts, verification and reset delivery through an already configured sender, one user role, and ownership checks for one existing resource. Include staging deployment, tests and a short handover document.
Before the sprint: agree account rules, provide repository and deployment access, choose the database, and make the email sender available.
First part: implement the account schema and migrations, session handling, signup/sign-in screens and server guards. Review the working flow on staging.
Second part: complete verification and recovery, exercise permissions and failure states, verify production configuration, and document account support tasks.
Exclude unless agreed: legacy account migration, enterprise SSO, organization billing, several role hierarchies, custom mobile clients and a separate security audit.
Unresolved sender setup, an unknown existing database or changing permission rules affect the estimate. Discover those before reserving implementation time. If the account model is still unsettled, start with a focused architecture review rather than trying to fit every decision into the build.
Define done with requests, not screenshots
An anonymous direct request to each protected endpoint is rejected.
A valid account can sign in, refresh a protected page and sign out; replaying its revoked session no longer works.
A wrong password is rejected without logging the password or leaking unnecessary account details.
Verification and reset emails arrive on the deployed domain, their links reach the intended app, and expired or reused tokens fail.
A second account cannot read or write another person’s resources, including through direct requests.
Production rate limits, origin checks and cookie settings work behind the deployed proxy.
A database or email outage gives a useful failure state, and retrying does not create duplicate business records.
The refresh fixture passed type checking and a production build. HTTP tests covered signup, a session cookie, the hook-to-locals path, anonymous access rejection, a wrong password, sign-out and rejection of the revoked cookie. It also checked that /api/me omits email and session tokens. Email delivery, OAuth, tenant isolation, production throttling and a login UI were not exercised by that fixture; the checklist above is work to verify in the actual app.
Scope your authentication work
Updraft’s four-week MVP included authentication and account management alongside the course interface and progress dashboard. The Updraft case study also describes how the authentication approach changed when multi-tenant features arrived.
If you need help choosing an account model or debugging sessions, a $149 consulting session is a smaller starting point. For implementation, my two-week sprint is $6,000, booked separately with no automatic renewal. Describe the app, current auth and the flow you need; I will confirm scope and availability before payment.
For the request-validation side of signup and settings screens, continue with SvelteKit form validation with Zod. Keep validation and authorization separate: a valid project ID is still not permission to edit that project.