Lucia vs NextAuth in 2026: Lightweight vs Full-Featured Auth
·PkgPulse Team
TL;DR
NextAuth for quick setup with OAuth; Lucia for full control over session management. NextAuth/Auth.js (~2.5M weekly downloads) handles OAuth providers, database sessions, and JWT out of the box — ideal for apps needing social login quickly. Lucia (~500K downloads) is a minimal session management library that gives you complete control over how auth works — ideal for apps with custom auth logic or email/password-focused auth.
Key Takeaways
- NextAuth/Auth.js: ~2.5M weekly downloads — Lucia: ~500K (npm, March 2026)
- Lucia only handles sessions — you write all auth logic (login, signup, validation)
- NextAuth handles OAuth providers — GitHub, Google, etc. in 5 lines
- Lucia v3 is framework-agnostic — not React/Next.js specific
- Lucia has better TypeScript — fully typed session/user objects without hacks
What Each Library Does
NextAuth (Auth.js):
✓ OAuth provider integration (80+ providers)
✓ Database session management via adapters
✓ JWT session tokens
✓ Callback hooks (signIn, session, jwt)
✓ CSRF protection
✗ Email/password (you add CredentialsProvider — less ideal)
✗ Session invalidation per-device
Lucia:
✓ Session creation and validation
✓ Cookie management
✓ Session invalidation (individual, all-for-user, all)
✓ Framework adapters (Hono, Astro, SvelteKit, Express, Next.js)
✗ OAuth providers (use arctic.js alongside Lucia)
✗ CSRF protection (handle yourself)
✗ "Magic link" / email verification (implement yourself)
Auth Flow Comparison
// NextAuth — OAuth in minutes
// providers: GitHub + Google in 10 lines
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({ clientId: '...', clientSecret: '...' }),
Google({ clientId: '...', clientSecret: '...' }),
],
adapter: DrizzleAdapter(db),
});
// Email/password — less clean, requires CredentialsProvider
providers: [
Credentials({
credentials: { email: {}, password: {} },
authorize: async ({ email, password }) => {
const user = await getUserByEmail(email as string);
if (!user || !verifyPassword(password as string, user.passwordHash)) {
return null; // Auth fails
}
return user;
},
}),
]
// Note: CredentialsProvider doesn't play well with database sessions
// Must use JWT strategy with credentials
// Lucia — full control, you build each auth flow
// 1. Session creation (after verifying credentials yourself)
import { lucia } from '@/lib/lucia';
// In your sign-in handler:
async function signIn(email: string, password: string) {
const user = await db.query.users.findFirst({ where: eq(users.email, email) });
if (!user || !await verifyPassword(password, user.passwordHash)) {
throw new Error('Invalid credentials');
}
const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect('/dashboard');
}
// 2. Validate session (in middleware or route handlers)
async function validateRequest() {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) return { user: null, session: null };
const { user, session } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
// Extend session on activity
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
return { user, session };
}
// 3. Sign out
async function signOut() {
const { session } = await validateRequest();
if (!session) return;
await lucia.invalidateSession(session.id);
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect('/login');
}
OAuth with Lucia + Arctic
// Lucia + Arctic (OAuth library by the same author)
import { GitHub } from 'arctic';
const github = new GitHub(clientId, clientSecret);
// Generate auth URL
const state = generateState();
const url = await github.createAuthorizationURL(state, {
scopes: ['user:email'],
});
// Handle callback
const tokens = await github.validateAuthorizationCode(code);
const response = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
const githubUser = await response.json();
// Create or get user from DB
let user = await db.query.users.findFirst({
where: eq(oauthAccounts.providerUserId, String(githubUser.id)),
});
if (!user) {
user = await db.transaction(async (tx) => {
// Create user and oauth link in DB
...
});
}
// Create Lucia session
const session = await lucia.createSession(user.id, {});
// ...
TypeScript Types
// NextAuth — session types require module augmentation
declare module 'next-auth' {
interface Session {
user: { id: string } & DefaultSession['user'] // Manual extension
}
}
// Lucia — session type is exact, no augmentation needed
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: {
email: string;
name: string;
};
}
}
// After registration:
const session = await lucia.validateSession(id);
session.user.email; // Fully typed, no any
session.user.name; // TypeScript knows this exists
When to Choose
Choose NextAuth/Auth.js when:
- Primary auth method is OAuth (GitHub, Google, etc.)
- You want auth setup in under an hour
- Your app doesn't need custom session management
- Email/password is secondary to social login
Choose Lucia when:
- Email/password is your primary auth method
- You need granular control over sessions (per-device invalidation, etc.)
- Building custom auth flows (magic links, passkeys, etc.)
- TypeScript correctness is a top priority
- Framework-agnostic auth library is needed
Compare Lucia and NextAuth package health on PkgPulse.
See the live comparison
View lucia vs. nextauth on PkgPulse →