I’ve been building web applications for years, and authentication always felt like the necessary evil. It was either too heavy, too complex, or too insecure. Recently, I kept hitting walls with cookie-based sessions in SvelteKit that felt clunky, or JWT setups that required too much boilerplate. That’s when I started looking for something that felt native to the framework. That search led me to Lucia. It promised a simpler way, built with TypeScript and for modern frameworks. Let me show you what I found.
The core idea is straightforward. Lucia manages user sessions directly in your database. This is different from storing a token in local storage. Your database becomes the single source of truth for who is logged in. When a user logs in, Lucia creates a session record. It then sends a secure, encrypted session cookie to the browser. Every subsequent request sends that cookie back. Your server code asks Lucia to validate it against the database. This method gives you control and clarity.
Why does this matter for SvelteKit? Because SvelteKit is designed to handle server and client logic as one cohesive unit. Lucia plugs directly into that flow. Your form actions for login and signup become clean. Your load functions can securely check for a user session before rendering a page. It feels like they were made for each other.
Setting it up begins with installation. You’ll need the core library and an adapter for your database.
npm install lucia @lucia-auth/adapter-postgresql
npm install @lucia-auth/adapter-mysql # or for MySQL
Next, you initialize Lucia in a dedicated server-side file, like src/lib/server/auth.ts. This is where you configure your database adapter and define what your user object looks like.
// src/lib/server/auth.ts
import { lucia } from "lucia";
import { postgres } from "@lucia-auth/adapter-postgresql";
import { sveltekit } from "lucia/middleware";
import { dev } from "$app/environment";
import { pool } from "$lib/server/db"; // Your database connection
export const auth = lucia({
adapter: postgres(pool, {
user: "auth_user",
key: "user_key",
session: "user_session"
}),
middleware: sveltekit(),
env: dev ? "DEV" : "PROD",
getUserAttributes: (data) => {
return {
username: data.username,
email: data.email
};
}
});
export type Auth = typeof auth;
See how we define the getUserAttributes? This is the first taste of type safety. Lucia knows the shape of your user data. This type will flow through your entire application.
Now, think about a login page. You have a simple form. The magic happens in the form action on the server. Here’s how a login action might look in your +page.server.ts file.
// src/routes/login/+page.server.ts
import { auth } from "$lib/server/auth";
import { LuciaError } from "lucia";
import { fail, redirect } from "@sveltejs/kit";
export const actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const username = formData.get("username");
const password = formData.get("password");
// Basic validation
if (!username || !password) {
return fail(400, { message: "Missing credentials" });
}
try {
// 1. Find the user key (e.g., username:password)
const key = await auth.useKey(
"username",
username.toString(),
password.toString()
);
// 2. Create a new session for that user
const session = await auth.createSession({
userId: key.userId,
attributes: {} // You can store IP, user agent here
});
// 3. Create the session cookie
const sessionCookie = auth.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes
});
} catch (e) {
if (e instanceof LuciaError) {
// Handle specific errors like AUTH_INVALID_KEY
return fail(400, { message: "Incorrect username or password" });
}
return fail(500, { message: "An unknown error occurred" });
}
// On success, redirect to a protected page
throw redirect(302, "/dashboard");
}
};
The process is clear: validate the key, create a session, set the cookie. But what happens on the next request to /dashboard? How does SvelteKit know a user is logged in? This is where load functions and type safety truly shine.
In your dashboard page, you can get the user session in the server load function. If there’s no valid session, you redirect to login.
// src/routes/dashboard/+page.server.ts
import { auth } from "$lib/server/auth";
import { redirect } from "@sveltejs/kit";
export const load = async ({ locals }) => {
const session = await locals.auth.validate();
if (!session) {
throw redirect(302, "/login");
}
// The user object is fully typed!
const user = session.user;
return { user };
};
How does locals.auth get there? This is handled by a SvelteKit hook. Hooks allow you to run code for every request. We use this to validate the session cookie and make the user data available in locals.
// src/hooks.server.ts
import { auth } from "$lib/server/auth";
export const handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(auth.sessionCookieName);
if (!sessionId) {
event.locals.auth = auth.handleRequest(event);
return resolve(event);
}
// Validate the session
const { session, user } = await auth.validateSession(sessionId);
if (session && session.fresh) {
// If the session was rotated, set the new cookie
const sessionCookie = auth.createSessionCookie(session.id);
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes
});
}
if (!session) {
// If the session is invalid, create a blank cookie to clear it
const sessionCookie = auth.createBlankSessionCookie();
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes
});
}
// Attach the auth request handler and user to locals
event.locals.auth = auth.handleRequest(event);
event.locals.user = user;
return resolve(event);
};
With this hook in place, every route in your app has access to locals.user. Your load functions and actions are protected. The best part? The user object is typed based on the attributes you defined in the getUserAttributes function back in the auth.ts setup. Your editor will autocomplete user.username and user.email.
This approach removes guesswork. You are not parsing a JWT payload and hoping the data is there. The database is queried, and the returned user object matches your defined schema. It makes the code more predictable and easier to debug.
So, is this just for simple apps? Not at all. Lucia handles session rotation, which is a key security practice for preventing session fixation attacks. It can manage password hashing, OAuth integration, and email verification. It scales because the logic is simple and the data lives in your own database. You own the entire flow.
Moving from abstract concepts to concrete code changes how you think about security. You see the direct link between the user action, the database record, and the session state. For me, this clarity is the biggest win. It turns authentication from a mysterious black box into a logical part of my application architecture.
I encourage you to try this setup. Start a new SvelteKit project and add Lucia. Feel how the types guide you. See how few lines of code it takes to add a protected route. It might just change how you view authentication, too. If this approach resonates with you, or if you have a different method you prefer, I’d love to hear about it. Share your thoughts in the comments below.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva