← All articles
AUTHENTICATION Lucia Auth: Flexible Authentication for TypeScript Apps 2026-03-04 · 5 min read · lucia · authentication · typescript

Lucia Auth: Flexible Authentication for TypeScript Apps

Authentication 2026-03-04 · 5 min read lucia authentication typescript sessions oauth nextjs hono astro security

Authentication libraries have two failure modes: doing too little (just session utilities, you implement everything) or too much (full-stack magic that breaks when you deviate from their assumptions). Lucia aims for a middle ground.

It provides session management, password hashing, and OAuth utilities — the hard parts — without prescribing your database schema, ORM, or framework. You adapt it to your stack rather than your stack adapting to it.

What Lucia Handles

What Lucia Doesn't Handle

Installation

npm install lucia arctic  # arctic is the OAuth library from the same author

Core Concepts

Lucia works with two entities:

You create a user record and a session record in your database. Lucia validates sessions on each request. That's the core loop.

Database Adapter

Lucia is database-agnostic. You configure an adapter for your database:

With Drizzle:

import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { db } from "./db";
import { sessions, users } from "./schema";

const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);

Your database needs user and session tables with specific columns. For Drizzle:

// schema.ts
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: text("id").primaryKey(),
  username: text("username").notNull().unique(),
  passwordHash: text("password_hash"),
});

export const sessions = pgTable("sessions", {
  id: text("id").primaryKey(),
  userId: text("user_id")
    .notNull()
    .references(() => users.id),
  expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
});

Initialize Lucia

// auth.ts
import { Lucia } from "lucia";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { db } from "./db";
import { sessions, users } from "./schema";

const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    attributes: {
      secure: process.env.NODE_ENV === "production",
    },
  },
  getUserAttributes: (attributes) => {
    return {
      username: attributes.username,
    };
  },
});

// TypeScript: declare module for type safety
declare module "lucia" {
  interface Register {
    Lucia: typeof lucia;
    DatabaseUserAttributes: typeof users.$inferSelect;
  }
}

Email/Password Authentication

Lucia doesn't generate signup/login routes — you write them. Here's a complete signup handler with Next.js API routes:

// app/api/auth/signup/route.ts
import { generateId } from "lucia";
import { hash } from "@node-rs/argon2";
import { lucia } from "@/lib/auth";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { cookies } from "next/headers";

export async function POST(request: Request) {
  const body = await request.json();
  const { username, password } = body;

  // Validate input
  if (!username || username.length < 3) {
    return Response.json({ error: "Invalid username" }, { status: 400 });
  }
  if (!password || password.length < 8) {
    return Response.json({ error: "Invalid password" }, { status: 400 });
  }

  const userId = generateId(15);
  const passwordHash = await hash(password, {
    memoryCost: 19456,
    timeCost: 2,
    outputLen: 32,
    parallelism: 1,
  });

  try {
    await db.insert(users).values({
      id: userId,
      username,
      passwordHash,
    });

    const session = await lucia.createSession(userId, {});
    const sessionCookie = lucia.createSessionCookie(session.id);
    (await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);

    return Response.json({ success: true });
  } catch (e) {
    // Handle unique constraint violation
    return Response.json({ error: "Username already taken" }, { status: 400 });
  }
}

Session Validation Middleware

Validate the session on each request:

// lib/auth/validate-request.ts
import { lucia } from "@/lib/auth";
import { cookies } from "next/headers";
import { cache } from "react";

export const validateRequest = cache(async () => {
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;

  if (!sessionId) {
    return { user: null, session: null };
  }

  const { user, session } = await lucia.validateSession(sessionId);

  try {
    if (session?.fresh) {
      // Extend session if still valid
      const sessionCookie = lucia.createSessionCookie(session.id);
      (await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
    }
    if (!session) {
      const sessionCookie = lucia.createBlankSessionCookie();
      (await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
    }
  } catch {}

  return { user, session };
});

Use in server components:

const { user } = await validateRequest();
if (!user) redirect("/login");

OAuth with GitHub

Using Arctic for OAuth:

// lib/auth/oauth.ts
import { GitHub } from "arctic";

export const github = new GitHub(
  process.env.GITHUB_CLIENT_ID!,
  process.env.GITHUB_CLIENT_SECRET!
);

OAuth callback handler:

// app/api/auth/github/callback/route.ts
import { github, lucia } from "@/lib/auth";
import { OAuth2RequestError, generateCodeVerifier } from "arctic";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { generateId } from "lucia";
import { cookies } from "next/headers";
import { eq } from "drizzle-orm";

export async function GET(request: Request) {
  const url = new URL(request.url);
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");
  const storedState = (await cookies()).get("github_oauth_state")?.value ?? null;

  if (!code || !state || !storedState || state !== storedState) {
    return new Response(null, { status: 400 });
  }

  try {
    const tokens = await github.validateAuthorizationCode(code);
    const githubUserResponse = await fetch("https://api.github.com/user", {
      headers: { Authorization: `Bearer ${tokens.accessToken()}` },
    });
    const githubUser = await githubUserResponse.json();

    // Check if user exists
    const existingUser = await db.query.users.findFirst({
      where: eq(users.githubId, githubUser.id),
    });

    if (existingUser) {
      const session = await lucia.createSession(existingUser.id, {});
      const sessionCookie = lucia.createSessionCookie(session.id);
      (await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
      return new Response(null, { status: 302, headers: { Location: "/" } });
    }

    // Create new user
    const userId = generateId(15);
    await db.insert(users).values({
      id: userId,
      githubId: githubUser.id,
      username: githubUser.login,
    });

    const session = await lucia.createSession(userId, {});
    const sessionCookie = lucia.createSessionCookie(session.id);
    (await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
    return new Response(null, { status: 302, headers: { Location: "/" } });
  } catch (e) {
    if (e instanceof OAuth2RequestError) {
      return new Response(null, { status: 400 });
    }
    return new Response(null, { status: 500 });
  }
}

Lucia vs. Auth.js (NextAuth)

Aspect Lucia Auth.js (NextAuth)
Flexibility High (you write routes) High (but with conventions)
Magic Minimal More opinionated
Database Any via adapters Many adapters
Session approach Database sessions JWT or database sessions
Email/password Manual Credentials provider
OAuth Via Arctic library Built-in providers
TypeScript Excellent Good
Complexity Medium Medium

Auth.js (NextAuth v5) works well for projects that fit its conventions. Lucia gives more control when you need it — especially useful if you have specific session storage requirements or need non-standard authentication flows.

Lucia's documentation at lucia-auth.com includes framework-specific guides for Next.js, SvelteKit, Astro, Hono, and others.