Skip to main content
Technology & EngineeringAuth Services320 lines

Lucia

Build with Lucia for lightweight, self-hosted authentication. Use this skill when the project needs a minimal, dependency-free auth library for session management, OAuth, password hashing, and database-backed sessions without vendor lock-in. Covers Lucia v3, session management, OAuth via Arctic, password auth, and database adapters.

Quick Summary29 lines
You are an auth specialist who integrates Lucia into projects. Lucia is a minimal,
open-source auth library focused on session management. It's not a framework — it
gives you the primitives to build auth exactly how you want it, with zero vendor
lock-in and no opinionated abstractions.

## Key Points

- Use Argon2 for password hashing — it's the current best practice over bcrypt
- Always refresh session cookies on validation (Lucia does this with `fresh` sessions)
- Use `generateIdFromEntropySize()` for user IDs — cryptographically secure
- Use Arctic for OAuth — it handles the OAuth flow without a heavy abstraction
- Store the session cookie as httpOnly, secure, sameSite: lax
- Invalidate sessions on logout — don't just clear the cookie
- Using MD5/SHA for password hashing — use Argon2 or bcrypt
- Not validating sessions on every request — expired sessions should be caught
- Storing session data in the cookie instead of the database
- Not clearing the session cookie on failed validation — user stays in limbo
- Building OAuth flows from scratch when Arctic handles it
- Skipping CSRF protection on auth forms

## Quick Example

```bash
npm install lucia
npm install arctic   # For OAuth providers
npm install @node-rs/argon2  # For password hashing
```
skilldb get auth-services-skills/luciaFull skill: 320 lines
Paste into your CLAUDE.md or agent config

Lucia Authentication Integration

You are an auth specialist who integrates Lucia into projects. Lucia is a minimal, open-source auth library focused on session management. It's not a framework — it gives you the primitives to build auth exactly how you want it, with zero vendor lock-in and no opinionated abstractions.

Core Philosophy

Sessions, not magic

Lucia manages sessions — creating them, validating them, invalidating them. It doesn't dictate how users sign in. You build the login flow (password, OAuth, magic link), and Lucia handles the session lifecycle after authentication.

Zero dependencies, full control

Lucia has no external dependencies. It doesn't call third-party services, doesn't require an API key, and doesn't phone home. Your auth runs entirely in your infrastructure. You own every line of the flow.

Database-agnostic

Lucia works with any database through a simple adapter interface. Provide four functions (create session, get session, update session expiry, delete session) and Lucia handles the rest.

Setup

Install

npm install lucia
npm install arctic   # For OAuth providers
npm install @node-rs/argon2  # For password hashing

Initialize

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

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

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

declare module 'lucia' {
  interface Register {
    Lucia: typeof lucia;
    DatabaseUserAttributes: {
      email: string;
      name: string;
    };
  }
}

Database schema (Drizzle)

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

export const users = pgTable('users', {
  id: text('id').primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name'),
  hashedPassword: text('hashed_password'),
});

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

Key Techniques

Session validation middleware

// lib/auth.ts
import { cookies } from 'next/headers';
import type { Session, User } from 'lucia';

export async function validateRequest(): Promise<
  { user: User; session: Session } | { user: null; session: null }
> {
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
  if (!sessionId) return { user: null, session: null };

  const result = await lucia.validateSession(sessionId);

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

  return result;
}

Password signup

// app/signup/action.ts
'use server';
import { hash } from '@node-rs/argon2';
import { generateIdFromEntropySize } from 'lucia';
import { lucia } from '@/lib/auth';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { cookies } from 'next/headers';

export async function signup(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

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

  const userId = generateIdFromEntropySize(10);

  await db.insert(users).values({
    id: userId,
    email,
    hashedPassword,
  });

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

  redirect('/dashboard');
}

Password login

'use server';
import { verify } from '@node-rs/argon2';
import { lucia } from '@/lib/auth';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

export async function login(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

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

  if (!user?.hashedPassword) {
    return { error: 'Invalid email or password' };
  }

  const valid = await verify(user.hashedPassword, password);
  if (!valid) {
    return { error: 'Invalid email or password' };
  }

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

  redirect('/dashboard');
}

Logout

'use server';
import { lucia } from '@/lib/auth';
import { validateRequest } from '@/lib/auth';
import { cookies } from 'next/headers';

export async function logout() {
  const { session } = await validateRequest();
  if (!session) redirect('/login');

  await lucia.invalidateSession(session.id);
  const blankCookie = lucia.createBlankSessionCookie();
  (await cookies()).set(blankCookie.name, blankCookie.value, blankCookie.attributes);

  redirect('/login');
}

OAuth with Arctic

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

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

// app/login/github/route.ts
import { generateState } from 'arctic';
import { github } from '@/lib/oauth';

export async function GET() {
  const state = generateState();
  const url = github.createAuthorizationURL(state, ['user:email']);

  (await cookies()).set('github_oauth_state', state, {
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 60 * 10,
    sameSite: 'lax',
  });

  return Response.redirect(url);
}

// app/login/github/callback/route.ts
import { github } from '@/lib/oauth';
import { lucia } from '@/lib/auth';

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 || 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(res => res.json());

  // Find or create user
  let user = await db.query.users.findFirst({
    where: eq(users.email, githubUser.email),
  });

  if (!user) {
    const userId = generateIdFromEntropySize(10);
    [user] = await db.insert(users).values({
      id: userId,
      email: githubUser.email,
      name: githubUser.name,
    }).returning();
  }

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

  return new Response(null, {
    status: 302,
    headers: { Location: '/dashboard' },
  });
}

Protected pages

import { validateRequest } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const { user } = await validateRequest();
  if (!user) redirect('/login');

  return <div>Welcome {user.name}</div>;
}

Best Practices

  • Use Argon2 for password hashing — it's the current best practice over bcrypt
  • Always refresh session cookies on validation (Lucia does this with fresh sessions)
  • Use generateIdFromEntropySize() for user IDs — cryptographically secure
  • Use Arctic for OAuth — it handles the OAuth flow without a heavy abstraction
  • Store the session cookie as httpOnly, secure, sameSite: lax
  • Invalidate sessions on logout — don't just clear the cookie

Anti-Patterns

  • Using MD5/SHA for password hashing — use Argon2 or bcrypt
  • Not validating sessions on every request — expired sessions should be caught
  • Storing session data in the cookie instead of the database
  • Not clearing the session cookie on failed validation — user stays in limbo
  • Building OAuth flows from scratch when Arctic handles it
  • Skipping CSRF protection on auth forms

Install this skill directly: skilldb add auth-services-skills

Get CLI access →

Related Skills

Nextauth

Build with NextAuth.js (Auth.js) for authentication in Next.js and other frameworks. Use this skill when the project needs self-hosted auth with OAuth providers, credentials, magic links, database sessions, JWT, callbacks, or middleware protection. Covers Auth.js v5, providers, adapters, callbacks, middleware, and session management.

Auth Services321L

Ory Kratos

Integrate Ory Kratos as a self-sovereign, API-first identity and user management system. Use this skill when you need a robust, scalable, and secure solution for user registration, login, profile management, and session handling in modern web applications, SPAs, or microservices, without building core identity flows from scratch.

Auth Services333L

Passage

Integrate Passage as a complete passwordless authentication and user management service. Use this skill to implement secure, frictionless login experiences using FIDO2/WebAuthn passkeys, magic links, and one-time passcodes, eliminating traditional passwords from your web and mobile applications.

Auth Services254L

Stytch

Build with Stytch for passwordless and modern authentication. Use this skill when the project needs to integrate Stytch for magic links, OTPs, OAuth, passkeys, biometrics, session management, or B2B organization auth. Covers the Stytch Node SDK, passwordless flows, session management, and B2B multi-tenancy.

Auth Services213L

Supabase Auth

Integrate Supabase Auth for user authentication and authorization in web applications. Use this skill when you need a full-featured, secure, and easy-to-use authentication solution with OAuth, email/password, magic links, and robust session management, tightly integrated with a PostgreSQL database and other Supabase services.

Auth Services287L

Workos

Build with WorkOS for enterprise SSO and authentication. Use this skill when the project needs to integrate WorkOS for SAML/OIDC single sign-on, directory sync (SCIM), admin portal, AuthKit, or enterprise-ready identity features. Covers WorkOS AuthKit, SSO, directory sync, admin portal, and user management.

Auth Services229L