~ 3 min read
Node.js Authentication from Lucia to Better Auth

Lucia started off as an educational project to teach developers about the fundamental aspects of authentication and authorization. It was created alongside with an implementation guide which then evolved into an official SDK, supporting different database persistency drivers and authentication schemes. However, the maintainer had stepped back from active development and archived the project. What should developers migrate to?
In this article I’ll cover the basics of Lucia authentication in Node.js and JavaScript backends and how to migrate from a Lucia Auth codebase to Better Auth, which is another open-source and well-maintained SDK. $$ $$
Lucia Authentication in Node.js
Lucia started with an auth.js
file that exported the database drivers interface and defined the way Lucia manages authentication via session cookie.
The following is a reference file fo how to build a Node.js backend authentication based on SQLite and the BetterSQLite3 database library.
import { Lucia } from "lucia";import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";import { db } from "./db.js";
import type { DatabaseUser } from "./db.js";
const adapter = new BetterSqlite3Adapter(db, { user: "user", session: "session"});
export const lucia = new Lucia(adapter, { sessionCookie: { attributes: { secure: process.env.NODE_ENV === "production" } }, getUserAttributes: (attributes) => { return { username: attributes.username }; }});
declare module "lucia" { interface Register { Lucia: typeof lucia; DatabaseUserAttributes: Omit<DatabaseUser, "id">; }}
The Login and Signup workflows used to require specific code to be written that integrates with Lucia. For example, here’s the Lucia code for implementing login:
import { lucia } from "../lib/auth.js";
import type { DatabaseUser } from "../lib/db.js";
export const loginRouter = express.Router();
loginRouter.post("/login", async (req, res) => { const username: string | null = req.body.username ?? null; const password: string | null = req.body.password ?? null;
const existingUser = await db().get("SELECT * FROM user WHERE username = ?", username) if (!existingUser) { return res.status(400).json({ error: "Invalid username or password" }); }
const validPassword = await verify(existingUser.password_hash, password); if (!validPassword) { return res.status(400).json({ error: "Invalid username or password" }); }
const session = await lucia.createSession(existingUser.id, {}); return res.appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) .status(200) .json({ success: true, })});
Migrating to Better Auth in Node.js
The version of auth.js
in Better Auth exports a similar interface that declares the database driver as well as other authentication information such as CORS if required to be defined (via the trustedOrigins
property), the authentication type (in this case it is emailAndPassword
) and any plugins we want to connect (in this case, OpenAPI to expose a Swagger like UI that is convenient for development-time).
import { betterAuth } from "better-auth";import { openAPI } from "better-auth/plugins";import Database from "better-sqlite3";
export const auth = betterAuth({ database: new Database("./database.db"), trustedOrigins: [ "http://localhost:3000", "http://localhost:3005", ], emailAndPassword: { enabled: true }, plugins: [ openAPI(), ]});
Unlike Lucia, Better Auth manages the Login and other authentication workflows through its own internal middle-ware function for Express and other web application frameworks, so it doesn’t require you to implement it, and instead if exposes an API.
import { db, init } from "./lib/db.js";
import { toNodeHandler } from "better-auth/node";import { fromNodeHeaders } from "better-auth/node";import { auth } from "./lib/auth.ts";
// establish a db connection firstawait init();
const app = express();
app.all("/api/auth/*", toNodeHandler(auth));
User session data can then be accessed and used as follows in an Express-like middleware function:
res.locals.user = session?.user;
if (session?.user && session?.user?.id) { const userAttribute = await db().get('SELECT role FROM user_profile WHERE user_id = ?', session.user.id); if (session?.user?.id && userAttribute?.role === 'admin') { res.locals.user.isAdmin = true; } }