Claude Code for Lucia: Database-Backed Session Authentication — Claude Skills 360 Blog
Blog / Backend / Claude Code for Lucia: Database-Backed Session Authentication
Backend

Claude Code for Lucia: Database-Backed Session Authentication

Published: May 13, 2027
Read time: 7 min read
By: Claude Skills 360

Lucia is a TypeScript authentication library for database-backed sessions — new Lucia(adapter, options) creates the instance. lucia.createSession(userId, {}) creates a new session and returns a Session. lucia.validateRequestCookie(Request) validates the session cookie and returns { session, user }. lucia.createSessionCookie(session.id) creates the cookie header. lucia.invalidateSession(sessionId) logs out. lucia.createBlankSessionCookie() clears the cookie. The database adapter (Drizzle, Prisma, MongoDB, etc.) stores sessions in your schema. getUserAttributes maps database columns to User. Arctic is Lucia’s companion OAuth library — arctic.GitHub(clientId, clientSecret), arctic.Google(...), and others generate authorization URLs and exchange codes for tokens. Passwords use Argon2 via argon2 package. Multi-device: lucia.getUserSessions(userId) lists all active sessions. Claude Code generates Lucia email/password auth, OAuth with GitHub/Google, session middleware, and protected page patterns.

CLAUDE.md for Lucia

## Lucia Stack
- Version: lucia >= 3.2, arctic >= 1.9, @lucia-auth/adapter-drizzle >= 1.1
- Init: const lucia = new Lucia(adapter, { sessionCookie: { attributes: { secure: isProd } }, getUserAttributes: (attrs) => ({ ...attrs }) })
- Create: const session = await lucia.createSession(userId, {}); const cookie = lucia.createSessionCookie(session.id)
- Validate: const { session, user } = await lucia.validateRequestCookie(reqCookies) — cookies() in Next.js
- Invalidate: await lucia.invalidateSession(sessionId)
- Blank: lucia.createBlankSessionCookie() — clear on logout
- Types: declare module "lucia" { interface Register { Lucia: typeof lucia; DatabaseUserAttributes: UserRow } }

Lucia Initialization

// lib/auth/lucia.ts — Lucia instance with Drizzle adapter
import { Lucia } from "lucia"
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"
import { db } from "@/lib/db"
import { sessions, users } from "@/lib/db/schema"

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

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    name: "auth_session",
    attributes: {
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
    },
  },
  sessionExpiresIn: new TimeSpan(30, "d"),
  getUserAttributes: (attributes) => ({
    id: attributes.id,
    email: attributes.email,
    name: attributes.name,
    role: attributes.role,
    avatarUrl: attributes.avatarUrl,
    emailVerified: attributes.emailVerified,
  }),
})

// TypeScript — register Lucia types
declare module "lucia" {
  interface Register {
    Lucia: typeof lucia
    DatabaseUserAttributes: {
      id: string
      email: string
      name: string
      role: "user" | "admin"
      avatarUrl: string | null
      emailVerified: boolean
    }
  }
}

Database Schema (Drizzle)

// lib/db/schema/auth.ts — Lucia-compatible tables
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"

export const users = pgTable("users", {
  id: text("id").primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name").notNull(),
  passwordHash: text("password_hash"),
  role: text("role", { enum: ["user", "admin"] }).notNull().default("user"),
  avatarUrl: text("avatar_url"),
  emailVerified: boolean("email_verified").notNull().default(false),
  githubId: text("github_id").unique(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
})

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

Auth Server Actions

// lib/auth/actions.ts — Server Actions for login/register/logout
"use server"
import { lucia } from "@/lib/auth/lucia"
import { db } from "@/lib/db"
import { users, sessions } from "@/lib/db/schema"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { hash, verify } from "@node-rs/argon2"
import { generateId } from "lucia"
import { eq } from "drizzle-orm"
import { z } from "zod"

const argon2Config = {
  memoryCost: 19456,   // 19 MiB
  timeCost: 2,
  outputLen: 32,
  parallelism: 1,
}

// Login action
export async function login(formData: FormData) {
  const parsed = z.object({
    email: z.string().email(),
    password: z.string().min(1),
  }).safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  })

  if (!parsed.success) {
    return { error: "Invalid input" }
  }

  const { email, password } = parsed.data

  const user = await db.query.users.findFirst({
    where: eq(users.email, email.toLowerCase()),
  })

  if (!user || !user.passwordHash) {
    // Timing-safe: hash even on not found to prevent enumeration
    await hash("dummy", argon2Config)
    return { error: "Invalid email or password" }
  }

  const validPassword = await verify(user.passwordHash, password, argon2Config)
  if (!validPassword) {
    return { error: "Invalid email or password" }
  }

  const session = await lucia.createSession(user.id, {})
  const sessionCookie = lucia.createSessionCookie(session.id)

  const cookieStore = await cookies()
  cookieStore.set(
    sessionCookie.name,
    sessionCookie.value,
    sessionCookie.attributes,
  )

  redirect("/dashboard")
}

// Register action
export async function register(formData: FormData) {
  const parsed = z.object({
    email: z.string().email(),
    password: z.string().min(8, "Password must be at least 8 characters"),
    name: z.string().min(1).max(100),
  }).safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
    name: formData.get("name"),
  })

  if (!parsed.success) {
    return { error: parsed.error.issues[0]?.message ?? "Invalid input" }
  }

  const { email, password, name } = parsed.data

  const existing = await db.query.users.findFirst({
    where: eq(users.email, email.toLowerCase()),
  })
  if (existing) {
    return { error: "Email already registered" }
  }

  const userId = generateId(15)
  const passwordHash = await hash(password, argon2Config)

  await db.insert(users).values({
    id: userId,
    email: email.toLowerCase(),
    name,
    passwordHash,
    role: "user",
    emailVerified: false,
  })

  // Auto-login after register
  const session = await lucia.createSession(userId, {})
  const sessionCookie = lucia.createSessionCookie(session.id)
  const cookieStore = await cookies()
  cookieStore.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes)

  redirect("/dashboard")
}

// Logout action
export async function logout() {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get(lucia.sessionCookieName)?.value

  if (sessionId) {
    await lucia.invalidateSession(sessionId)
  }

  const blankCookie = lucia.createBlankSessionCookie()
  cookieStore.set(blankCookie.name, blankCookie.value, blankCookie.attributes)

  redirect("/sign-in")
}

Session Validation Utility

// lib/auth/session.ts — validate session in Server Components and middleware
import { lucia } from "./lucia"
import { cookies } from "next/headers"
import { cache } from "react"

// cache() deduplicates across a single request
export const validateSession = cache(async () => {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get(lucia.sessionCookieName)?.value

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

  const result = await lucia.validateSession(sessionId)

  // Refresh session cookie if close to expiry
  if (result.session?.fresh) {
    const refreshedCookie = lucia.createSessionCookie(result.session.id)
    cookieStore.set(refreshedCookie.name, refreshedCookie.value, refreshedCookie.attributes)
  }

  if (!result.session) {
    const blankCookie = lucia.createBlankSessionCookie()
    cookieStore.set(blankCookie.name, blankCookie.value, blankCookie.attributes)
  }

  return result
})

// Require auth — throw redirect if not authenticated
export async function requireSession() {
  const { session, user } = await validateSession()
  if (!session || !user) {
    // Import redirect at call site to avoid Next.js issues
    const { redirect } = await import("next/navigation")
    redirect("/sign-in")
  }
  return { session, user }
}

GitHub OAuth with Arctic

// lib/auth/github.ts — GitHub OAuth with Arctic
import { GitHub } from "arctic"
import { lucia } from "./lucia"
import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { generateId } from "lucia"
import { eq } from "drizzle-orm"
import { cookies } from "next/headers"

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

// app/auth/github/route.ts — redirect to GitHub
export async function GET() {
  const state = generateId(40)
  const cookieStore = await cookies()
  cookieStore.set("github_oauth_state", state, { httpOnly: true, maxAge: 600, path: "/" })

  const url = await github.createAuthorizationURL(state, { scopes: ["user:email"] })
  return Response.redirect(url.toString())
}

// app/auth/github/callback/route.ts — exchange code for user
export async function handleGitHubCallback(request: Request) {
  const { searchParams } = new URL(request.url)
  const code = searchParams.get("code")
  const state = searchParams.get("state")

  const cookieStore = await cookies()
  const storedState = cookieStore.get("github_oauth_state")?.value

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

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

  // Get primary email
  const emails = await fetch("https://api.github.com/user/emails", {
    headers: { Authorization: `Bearer ${tokens.accessToken}` },
  }).then(r => r.json())
  const primaryEmail = emails.find((e: any) => e.primary)?.email

  // Upsert user
  let userId: string

  const existing = await db.query.users.findFirst({
    where: eq(users.githubId, String(githubUser.id)),
  })

  if (existing) {
    userId = existing.id
  } else {
    userId = generateId(15)
    await db.insert(users).values({
      id: userId,
      email: primaryEmail ?? `github-${githubUser.id}@placeholder.com`,
      name: githubUser.name ?? githubUser.login,
      githubId: String(githubUser.id),
      avatarUrl: githubUser.avatar_url,
      emailVerified: true,
      role: "user",
    })
  }

  const session = await lucia.createSession(userId, {})
  const cookie = lucia.createSessionCookie(session.id)
  cookieStore.set(cookie.name, cookie.value, cookie.attributes)

  return Response.redirect(new URL("/dashboard", request.url))
}

For the Better Auth alternative when multiple social providers, organizations/teams, API keys, two-factor Auth, and a richer plugin ecosystem out of the box are needed without writing adapter code — Better Auth handles more use cases with less configuration than Lucia, though Lucia gives finer control over session storage and behavior, see the Better Auth guide. For the Passport.js alternative when integrating auth into an existing Express.js application with an established strategy ecosystem covering 500+ providers including SAML and OpenID Connect — Passport.js has wider strategy coverage while Lucia is purpose-built for Next.js/Remix and newer TypeScript-first apps, see the Passport.js guide. The Claude Skills 360 bundle includes Lucia skill sets covering session auth, OAuth, and multi-device sessions. Start with the free tier to try Lucia authentication generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free