Packages
@thexjs/auth
Plug-and-play authentication for X apps. Add credentials (username/password) and OAuth2 — including a preconfigured GitHub provider — with one defineAuth() call, a sessions table in SQLite or Postgres via the framework's data layer, and a single catch-all API route.
bun add @thexjs/auth
Quick start
Define your providers and session store once:
import { defineAuth, createSQLiteSessionStore, hashPassword, verifyPassword } from "@thexjs/auth";export const auth = defineAuth({ secret: process.env.AUTH_SECRET!, store: createSQLiteSessionStore(), // or createPostgresSessionStore(client) providers: [ { id: "local", name: "Local", type: "credentials", async authorize({ email, password }) { const user = await db.query("SELECT * FROM users WHERE email = ?").get(email); if (!user) return null; if (!(await verifyPassword(password, user.password_hash))) return null; return { id: String(user.id), name: user.name, email: user.email }; }, }, { id: "github", name: "GitHub", type: "oauth", clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, ],});The github provider is a preset: give it a client ID and secret and the authorization, token, and user-info URLs are wired up for you. A generic OAuth2 provider is available too for any other authorization-code provider.
Wire it up with one catch-all API route:
import { auth } from "../../lib/auth";export async function POST(req: Request) { return auth.handleRequest(req);}export async function GET(req: Request) { return auth.handleRequest(req);}Create users at sign-up with hashPassword (Argon2id) and store the hash — never plaintext:
import { hashPassword } from "@thexjs/auth";await hashPassword("correct horse battery staple");Endpoints
handleRequest routes the path below api/auth:
Route Method Purpose──────────────────────────────────────────────────────────────/api/auth/signin/<id> POST credentials provider: form or multipart body with the provider's fields (e.g. email, password)/api/auth/signin/<id> GET OAuth2 provider: redirects the browser to the provider's authorization URL/api/auth/callback/<id> GET OAuth2 callback: exchanges the code, validates the state challenge, signs the user in/api/auth/signout POST revokes the session and clears the cookie/api/auth/session GET JSON { "user": { ... } } or 401A sign-in form POSTs to /api/auth/signin/localand, after success, the browser follows the 302 to successRedirect (default /). For OAuth, the button or link is just a GET to /api/auth/signin/github.
Reading the session
const session = await auth.getSession(request);if (!session) return new Response("Unauthorized", { status: 401 });session.user; // { id, name?, email? } snapshot from sign-ingetSession hashes the x_session cookie, looks up the token in the store, and returns null for expired or revoked sessions. setSessionCookie(res, user, provider) and clearSessionCookie(res, req?) are also exported for programmatic flows.
Security
- Passwords — Argon2id via Bun.password ( hashPassword / verifyPassword).
- Session tokens — opaque random strings; only an HMAC-SHA256 digest (keyed by secret) is stored, so a database leak doesn't expose usable session cookies. Tokens are random 128-bit values, revocable, and expire after sessionMaxAge (default 7 days).
- OAuth state — an x_oauth_state cookie challenge must match the state param on the callback (HMAC'd, 5-minute expiry), preventing login-CSRF and session-fixation via crafted callbacks.
- CSRF — POST endpoints ( signin, signout ) run the core checkCsrf automatically — Origin/Referer verification by default, or requireToken for double-submit defense in depth — and reject non-conforming requests with 403. See Security for how the module is configured.
- Cookies — HttpOnly, SameSite=Lax, Secure in production.
Set a stable secret in production. If omitted, a random per-process secret is generated and a warning is printed, which means sessions won't survive restarts.
Session stores
Both stores use a single x_sessions table and implement the SessionStore interface ( create, find, revoke), so you can bring your own:
createSQLiteSessionStore({ path: "data/auth.db" }); // default: data/auth.dbcreatePostgresSessionStore(connectPostgres({ url: process.env.DATABASE_URL }));The Postgres store ensures the table lazily on first use and takes a client returned by connectPostgres from @thexjs/core/data, so it inherits the connection pool, TLS policy, and retry behavior of the framework. See Data Layer for the underlying stores.