Lucia Auth

authentication libraryLucia AuthJavaScriptTypeScriptsession managementlightweight authenticationdatabase integrationframework-agnostic

Authentication Library

Lucia Auth

Overview

Lucia Auth is a lightweight open-source authentication library gaining attention in 2025. Specializing in session-based authentication, it features a simple yet highly flexible design. With full TypeScript support, database-agnostic, and framework-neutral architecture, it works across diverse frameworks including Next.js, SvelteKit, Astro, Solid, and Nuxt. By eliminating complex configurations and providing a minimal API that allows developers full control over authentication logic, it delivers built-in security best practices.

Details

Lucia Auth is a next-generation authentication library designed under the philosophy that "authentication should not be complex." It provides authentication foundations including password hashing, session management, CSRF protection, and secure cookie generation as core features, enabling flexible data persistence through database adapters (Prisma, Drizzle, MongoDB, Firebase, etc.). Unlike traditional libraries, its design that doesn't impose authentication flows or UI elements allows developers to freely customize authentication systems according to requirements. Session-based authentication avoids JWT management complexity, providing a more intuitive and secure authentication experience.

Key Features

  • Lightweight Design: High-performance authentication features with minimal bundle size
  • Framework Neutral: Unified API usage across React, Vue, Svelte, etc.
  • Database Agnostic: Rich database adapter support
  • Full TypeScript Support: End-to-end type safety
  • Session-Based: Simple session management without JWT requirements
  • Developer Friendly: Rapid deployment with minimal configuration

Advantages and Disadvantages

Advantages

  • Reduced learning cost through simple and understandable API
  • Framework neutrality enables reusability across diverse environments
  • Lightweight design minimizes bundle size and enables high-speed operation
  • Excellent development-time type safety with full TypeScript support
  • Flexible integration through rich database adapters
  • High customizability through complete control of authentication flows

Disadvantages

  • Limited track record and community as a relatively new library
  • Advanced features (SSO, MFA, etc.) require custom implementation
  • Documentation is in development with limited learning resources
  • No enterprise support or SLA provided
  • Limited OAuth2.0/OpenID Connect support
  • Concerns about long-term support in large-scale projects

Reference Pages

Usage Examples

Installation and Setup

# Install Lucia Auth
npm install lucia

# Install database adapter (example: Prisma)
npm install @lucia-auth/adapter-prisma prisma
npm install bcrypt
npm install @types/bcrypt # For TypeScript

# Other adapter examples
npm install @lucia-auth/adapter-drizzle  # Drizzle ORM
npm install @lucia-auth/adapter-mongodb  # MongoDB
npm install @lucia-auth/adapter-sqlite   # SQLite

Basic Configuration with Prisma

// lib/lucia.ts - Lucia Auth Configuration
import { lucia } from "lucia";
import { prisma } from "@lucia-auth/adapter-prisma";
import { PrismaClient } from "@prisma/client";
import { webcrypto } from "crypto";

// WebCrypto polyfill for Node.js environment
globalThis.crypto = webcrypto as Crypto;

const client = new PrismaClient();

export const auth = lucia({
	adapter: prisma(client, {
		user: "user", // Prisma model name
		key: "key",   // Prisma model name
		session: "session" // Prisma model name
	}),
	env: process.env.NODE_ENV === "development" ? "DEV" : "PROD",
	middleware: "node", // For Next.js
	sessionCookie: {
		expires: false // Persistent sessions
	},
	getUserAttributes: (data) => {
		return {
			// Include user attributes in session
			email: data.email,
			name: data.name,
			role: data.role
		};
	}
});

export type Auth = typeof auth;
// prisma/schema.prisma - Prisma Schema Definition
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql" // MySQL, SQLite also supported
  url      = env("DATABASE_URL")
}

model User {
  id       String    @id @unique
  email    String    @unique
  name     String?
  role     String    @default("user")
  
  auth_session Session[]
  key          Key[]
  
  @@map("users")
}

model Session {
  id             String @id @unique
  user_id        String
  active_expires BigInt
  idle_expires   BigInt
  user           User   @relation(references: [id], fields: [user_id], onDelete: Cascade)

  @@index([user_id])
  @@map("sessions")
}

model Key {
  id              String  @id @unique
  hashed_password String?
  user_id         String
  user            User    @relation(references: [id], fields: [user_id], onDelete: Cascade)

  @@index([user_id])
  @@map("keys")
}

User Registration Functionality

// lib/auth.ts - Authentication Helper Functions
import { auth } from "./lucia";
import { generateId } from "lucia";
import bcrypt from "bcrypt";

export const createUser = async (email: string, password: string, name?: string) => {
	try {
		// Hash password
		const hashedPassword = await bcrypt.hash(password, 12);
		
		// Create user
		const userId = generateId(15);
		const user = await auth.createUser({
			userId,
			key: {
				providerId: "email", // Provider ID
				providerUserId: email.toLowerCase(), // Provider user ID
				password: hashedPassword
			},
			attributes: {
				email: email.toLowerCase(),
				name: name || "",
				role: "user"
			}
		});
		
		return user;
	} catch (error) {
		throw new Error("User creation failed");
	}
};

export const signInUser = async (email: string, password: string) => {
	try {
		// User authentication
		const key = await auth.useKey("email", email.toLowerCase(), password);
		const session = await auth.createSession({
			userId: key.userId,
			attributes: {}
		});
		
		return session;
	} catch (error) {
		throw new Error("Authentication failed");
	}
};

export const signOutUser = async (sessionId: string) => {
	try {
		await auth.invalidateSession(sessionId);
	} catch (error) {
		throw new Error("Sign out failed");
	}
};

Next.js App Router Integration

// app/api/auth/signup/route.ts - User Registration API
import { NextRequest, NextResponse } from "next/server";
import { createUser } from "@/lib/auth";
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
	try {
		const { email, password, name } = await request.json();
		
		// Validation
		if (!email || !password) {
			return NextResponse.json(
				{ error: "Email and password are required" },
				{ status: 400 }
			);
		}
		
		if (password.length < 8) {
			return NextResponse.json(
				{ error: "Password must be at least 8 characters" },
				{ status: 400 }
			);
		}
		
		// Create user
		const user = await createUser(email, password, name);
		
		// Create session
		const session = await auth.createSession({
			userId: user.userId,
			attributes: {}
		});
		
		// Set session cookie
		const sessionCookie = auth.createSessionCookie(session);
		cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
		
		return NextResponse.json({
			success: true,
			user: {
				id: user.userId,
				email: user.email,
				name: user.name
			}
		});
	} catch (error) {
		return NextResponse.json(
			{ error: "Account creation failed" },
			{ status: 500 }
		);
	}
}
// app/api/auth/signin/route.ts - Sign In API
import { NextRequest, NextResponse } from "next/server";
import { signInUser } from "@/lib/auth";
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
	try {
		const { email, password } = await request.json();
		
		if (!email || !password) {
			return NextResponse.json(
				{ error: "Email and password are required" },
				{ status: 400 }
			);
		}
		
		// User authentication
		const session = await signInUser(email, password);
		
		// Set session cookie
		const sessionCookie = auth.createSessionCookie(session);
		cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
		
		return NextResponse.json({
			success: true,
			session: {
				id: session.sessionId,
				userId: session.user.userId
			}
		});
	} catch (error) {
		return NextResponse.json(
			{ error: "Authentication failed" },
			{ status: 401 }
		);
	}
}
// app/api/auth/signout/route.ts - Sign Out API
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
	try {
		const sessionId = cookies().get(auth.sessionCookieName)?.value ?? null;
		
		if (!sessionId) {
			return NextResponse.json({ error: "Session not found" }, { status: 401 });
		}
		
		// Invalidate session
		await auth.invalidateSession(sessionId);
		
		// Clear cookie
		const sessionCookie = auth.createBlankSessionCookie();
		cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
		
		return NextResponse.json({ success: true });
	} catch (error) {
		return NextResponse.json({ error: "Sign out failed" }, { status: 500 });
	}
}

Authentication Protection with Middleware

// middleware.ts - Next.js Middleware
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/lucia";

export async function middleware(request: NextRequest) {
	const sessionId = request.cookies.get(auth.sessionCookieName)?.value ?? null;
	
	// Define protected routes
	const protectedPaths = ["/dashboard", "/profile", "/admin"];
	const isProtectedRoute = protectedPaths.some(path => 
		request.nextUrl.pathname.startsWith(path)
	);
	
	if (isProtectedRoute) {
		if (!sessionId) {
			// Redirect to login if unauthenticated
			return NextResponse.redirect(new URL("/login", request.url));
		}
		
		try {
			// Validate session
			const { session, user } = await auth.validateSession(sessionId);
			
			if (!session) {
				// Redirect to login if invalid session
				return NextResponse.redirect(new URL("/login", request.url));
			}
			
			// Add user info to headers for valid session
			const response = NextResponse.next();
			response.headers.set("x-user-id", user.userId);
			response.headers.set("x-user-email", user.email);
			
			return response;
		} catch (error) {
			return NextResponse.redirect(new URL("/login", request.url));
		}
	}
	
	return NextResponse.next();
}

export const config = {
	matcher: ["/dashboard/:path*", "/profile/:path*", "/admin/:path*"]
};

Authentication Usage in React Components

// components/LoginForm.tsx - Login Form
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginForm() {
	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState("");
	const router = useRouter();
	
	const handleSubmit = async (e: React.FormEvent) => {
		e.preventDefault();
		setLoading(true);
		setError("");
		
		try {
			const response = await fetch("/api/auth/signin", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: JSON.stringify({ email, password }),
			});
			
			const data = await response.json();
			
			if (response.ok) {
				router.push("/dashboard");
				router.refresh();
			} else {
				setError(data.error || "Login failed");
			}
		} catch (error) {
			setError("Network error occurred");
		} finally {
			setLoading(false);
		}
	};
	
	return (
		<form onSubmit={handleSubmit} className="space-y-4">
			<div>
				<label htmlFor="email" className="block text-sm font-medium text-gray-700">
					Email
				</label>
				<input
					type="email"
					id="email"
					value={email}
					onChange={(e) => setEmail(e.target.value)}
					required
					className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
				/>
			</div>
			
			<div>
				<label htmlFor="password" className="block text-sm font-medium text-gray-700">
					Password
				</label>
				<input
					type="password"
					id="password"
					value={password}
					onChange={(e) => setPassword(e.target.value)}
					required
					className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
				/>
			</div>
			
			{error && (
				<div className="text-red-600 text-sm">{error}</div>
			)}
			
			<button
				type="submit"
				disabled={loading}
				className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
			>
				{loading ? "Signing in..." : "Sign In"}
			</button>
		</form>
	);
}

Authentication State Retrieval in Server Components

// app/dashboard/page.tsx - Dashboard Page
import { auth } from "@/lib/lucia";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
	const sessionId = cookies().get(auth.sessionCookieName)?.value ?? null;
	
	if (!sessionId) {
		redirect("/login");
	}
	
	try {
		const { session, user } = await auth.validateSession(sessionId);
		
		if (!session) {
			redirect("/login");
		}
		
		return (
			<div className="min-h-screen bg-gray-50">
				<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
					<div className="px-4 py-6 sm:px-0">
						<div className="border-4 border-dashed border-gray-200 rounded-lg p-6">
							<h1 className="text-3xl font-bold text-gray-900 mb-4">
								Dashboard
							</h1>
							
							<div className="bg-white shadow overflow-hidden sm:rounded-lg">
								<div className="px-4 py-5 sm:p-6">
									<h3 className="text-lg leading-6 font-medium text-gray-900">
										User Information
									</h3>
									<div className="mt-5 border-t border-gray-200">
										<dl className="divide-y divide-gray-200">
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													User ID
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.userId}
												</dd>
											</div>
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													Email
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.email}
												</dd>
											</div>
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													Name
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.name || "Not set"}
												</dd>
											</div>
											<div className="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
												<dt className="text-sm font-medium text-gray-500">
													Role
												</dt>
												<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
													{user.role}
												</dd>
											</div>
										</dl>
									</div>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		);
	} catch (error) {
		redirect("/login");
	}
}