Auth0 costs $23/month for 1,000 users. Clerk costs $25/month after 10,000 MAUs. NextAuth hides session management behind magic. You're either paying a premium or fighting abstractions.
What if authentication was just code you own? Sessions, cookies, password hashing — simple primitives you understand and control?
That's Lucia. A TypeScript auth library that gives you building blocks, not a black box.
What Lucia Is (and Isn't)
Lucia is NOT a drop-in solution like Auth0. It's a library that handles:
- Session management (create, validate, invalidate sessions)
- Cookie management (secure session cookies)
- Session token generation (cryptographically secure)
You provide:
- Database storage (any database — Lucia is adapter-based)
- Password hashing (Lucia recommends Argon2 or bcrypt)
- OAuth flows (if you want social login)
- UI (login forms, registration pages)
Quick Start — Email + Password Auth
import { Lucia } from "lucia";
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
// 1. Initialize Lucia with your database
const adapter = new DrizzleSQLiteAdapter(db, sessionTable, userTable);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production",
},
},
getUserAttributes: (attributes) => ({
email: attributes.email,
name: attributes.name,
}),
});
Registration
import { generateIdFromEntropySize } from "lucia";
import { hash } from "@node-rs/argon2";
async function signup(email: string, password: string) {
const passwordHash = await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
const userId = generateIdFromEntropySize(10);
await db.insert(userTable).values({
id: userId,
email,
passwordHash,
});
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
// Set cookie on response
return new Response(null, {
status: 302,
headers: {
Location: "/dashboard",
"Set-Cookie": sessionCookie.serialize(),
},
});
}
Session Validation (Middleware)
async function validateRequest(request: Request) {
const sessionId = lucia.readSessionCookie(request.headers.get("Cookie") ?? "");
if (!sessionId) return { user: null, session: null };
const result = await lucia.validateSession(sessionId);
if (result.session && result.session.fresh) {
// Session was extended — update cookie
const cookie = lucia.createSessionCookie(result.session.id);
// Set cookie on response
}
if (!result.session) {
const cookie = lucia.createBlankSessionCookie();
// Clear cookie on response
}
return result;
}
OAuth (Google, GitHub, etc.)
import { Google } from "arctic";
const google = new Google(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:3000/auth/google/callback"
);
// Step 1: Redirect to Google
app.get("/auth/google", async (req, res) => {
const [url, codeVerifier, state] = await google.createAuthorizationURL();
// Store codeVerifier and state in cookies
res.redirect(url.toString());
});
// Step 2: Handle callback
app.get("/auth/google/callback", async (req, res) => {
const code = req.query.code;
const tokens = await google.validateAuthorizationCode(code, codeVerifier);
const googleUser = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
}).then(r => r.json());
// Find or create user, create session
let user = await db.getUserByGoogleId(googleUser.sub);
if (!user) {
user = await db.createUser({ googleId: googleUser.sub, email: googleUser.email });
}
const session = await lucia.createSession(user.id, {});
const cookie = lucia.createSessionCookie(session.id);
res.setHeader("Set-Cookie", cookie.serialize());
res.redirect("/dashboard");
});
Lucia vs Auth0 vs Clerk vs NextAuth
| Feature | Lucia | Auth0 | Clerk | NextAuth |
|---|---|---|---|---|
| Cost | Free | $23/mo+ | $25/mo+ | Free |
| Self-hosted | Yes | No | No | Yes |
| Session control | Full | Limited | Limited | Moderate |
| Database freedom | Any | Their DB | Their DB | Any |
| UI components | None (BYO) | Included | Included | Basic |
| Vendor lock-in | None | High | High | Low |
When to Choose Lucia
Choose Lucia when:
- You want full control over your auth flow
- Vendor lock-in is unacceptable
- You need custom session logic (multi-device, session metadata)
- You're building a self-hosted product
Skip Lucia when:
- You need auth working in 5 minutes (use Clerk)
- You want pre-built UI components (Auth0, Clerk)
- Your team doesn't want to handle password hashing and security details
The Bottom Line
Lucia trades convenience for control. You write more code, but you own every part of your auth system — no surprise bills, no vendor lock-in, no black boxes.
Start here: lucia-auth.com
Need custom data extraction, scraping, or automation? I build tools that collect and process data at scale — 78 actors on Apify Store and 265+ open-source repos. Email me: Spinov001@gmail.com | My Apify Actors
Top comments (0)