How to Use Lucia with Astro: Complete Guide
Step-by-step guide to integrating Lucia with your Astro website.
Lucia is a lightweight, session-based authentication library for TypeScript. Unlike Auth0 or Clerk, which are full platforms with hosted UIs and user management dashboards, Lucia gives you the building blocks to implement auth yourself. You own the code, the sessions, the database schema, and the entire flow. If you prefer understanding exactly how your auth works instead of relying on a black box, Lucia is the library for you.
For Astro developers, Lucia integrates naturally with server-side code. You handle sessions in middleware, protect routes, and manage users directly in your database. There is no third-party redirect or widget to style around.
Prerequisites
- Node.js 18+
- An Astro project with SSR enabled (
npm create astro@latest) - A database (SQLite, PostgreSQL, MySQL, or MongoDB)
- TypeScript configured (Lucia is TypeScript-first)
Installation
Install Lucia and a database adapter. Here is the setup with SQLite via better-sqlite3:
npm install lucia @lucia-auth/adapter-sqlite better-sqlite3
npm install -D @types/better-sqlite3
For PostgreSQL:
npm install lucia @lucia-auth/adapter-postgresql pg
npm install -D @types/pg
If you want OAuth providers (GitHub, Google, etc.):
npm install arctic
Configuration
Set up your database schema. Lucia needs a user and session table:
-- For SQLite
CREATE TABLE user (
id TEXT NOT NULL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
username TEXT NOT NULL
);
CREATE TABLE session (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL REFERENCES user(id)
);
Initialize Lucia with your database adapter:
// src/lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
import Database from "better-sqlite3";
const db = new Database("sqlite.db");
const adapter = new BetterSqlite3Adapter(db, {
user: "user",
session: "session",
});
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: import.meta.env.PROD,
},
},
getUserAttributes: (attributes) => {
return {
email: attributes.email,
username: attributes.username,
};
},
});
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: {
email: string;
username: string;
};
}
}
export { db };
Make sure your Astro config uses SSR:
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
});
Basic Usage
Create a registration endpoint:
// src/pages/api/auth/register.ts
import type { APIRoute } from "astro";
import { lucia, db } from "../../../lib/auth";
import { generateId } from "lucia";
import { Argon2id } from "oslo/password";
export const POST: APIRoute = async ({ request, cookies }) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const username = formData.get("username") as string;
if (!email || !password || password.length < 8) {
return new Response("Invalid input", { status: 400 });
}
const hashedPassword = await new Argon2id().hash(password);
const userId = generateId(15);
try {
db.prepare(
"INSERT INTO user (id, email, hashed_password, username) VALUES (?, ?, ?, ?)"
).run(userId, email, hashedPassword, username);
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: { Location: "/dashboard" },
});
} catch {
return new Response("Email already exists", { status: 400 });
}
};
Create a login endpoint:
// src/pages/api/auth/login.ts
import type { APIRoute } from "astro";
import { lucia, db } from "../../../lib/auth";
import { Argon2id } from "oslo/password";
export const POST: APIRoute = async ({ request, cookies }) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const user = db.prepare("SELECT * FROM user WHERE email = ?").get(email);
if (!user) {
return new Response("Invalid credentials", { status: 400 });
}
const validPassword = await new Argon2id().verify(user.hashed_password, password);
if (!validPassword) {
return new Response("Invalid credentials", { status: 400 });
}
const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: { Location: "/dashboard" },
});
};
Protect routes with Astro middleware:
// src/middleware.ts
import { lucia } from "./lib/auth";
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => {
const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
context.locals.user = null;
context.locals.session = null;
return next();
}
const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!session) {
const sessionCookie = lucia.createBlankSessionCookie();
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
context.locals.user = user;
context.locals.session = session;
return next();
});
Use the user data in pages:
---
// src/pages/dashboard.astro
const user = Astro.locals.user;
if (!user) {
return Astro.redirect("/login");
}
---
<html>
<body>
<h1>Welcome, {user.username}</h1>
<p>Email: {user.email}</p>
<form method="post" action="/api/auth/logout">
<button type="submit">Logout</button>
</form>
</body>
</html>
Production Tips
Use Argon2id for password hashing. Lucia works with any hashing library, but Argon2id (via the
oslopackage) is the recommended choice. It is memory-hard, which makes brute-force attacks expensive.Implement session renewal. Lucia marks sessions as "fresh" when they are close to expiring. The middleware example above handles this automatically by setting a new cookie when a session is refreshed.
Add rate limiting to auth endpoints. Lucia does not include rate limiting. Use a library like
rate-limiter-flexibleor implement a simple counter in your database to prevent brute-force login attempts.Use Arctic for OAuth. The
arcticpackage provides OAuth helpers for GitHub, Google, Discord, and other providers. It handles the token exchange flow so you only need to configure the redirect URIs.Store sessions in your production database. For local development, SQLite is fine. For production, use the PostgreSQL or MySQL adapter so sessions persist across server restarts and scale across instances.
Alternatives to Consider
- Clerk if you want a hosted solution with pre-built UI components, social login, and an organization management dashboard.
- Auth0 if you need enterprise features like SSO, MFA, and compliance certifications without building them yourself.
- Supabase Auth if you are already using Supabase and want auth that integrates directly with your Postgres database and Row Level Security.
Wrapping Up
Lucia gives you full control over authentication without the complexity of building everything from scratch. It handles session management, cookie security, and database adapters while letting you own the implementation details. For Astro SSR sites where you want to avoid third-party auth services and keep user data in your own database, Lucia strikes the right balance between doing it yourself and reinventing the wheel. The learning curve is steeper than hosted solutions, but the result is auth that you understand completely and can customize without limits.
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.