TypeScript
ideal-auth is written in TypeScript and exports generic types that flow through the entire auth lifecycle. When you provide a user type to createAuth, every method that returns or accepts a user is typed accordingly.
Typing your user
Section titled “Typing your user”The createAuth function accepts a generic type parameter TUser that must extend AnyUser:
type AnyUser = { id: string | number; [key: string]: any };This means your user type needs at least an id field that is a string or number. Everything else is up to you.
import { createAuth } from 'ideal-auth';
interface User { id: string; email: string; name: string; role: 'admin' | 'user'; password: string;}
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, resolveUser: (id) => db.user.findUnique({ where: { id } }), resolveUserByCredentials: (creds) => db.user.findUnique({ where: { email: creds.email } }), hash,});
const session = auth();const user = await session.user(); // User | null
if (user) { user.role; // 'admin' | 'user' — fully typed user.email; // string user.name; // string}The generic flows through every method on the auth instance:
session.user()returnsPromise<User | null>session.login(user)expects aUsersession.attempt(creds)resolves the user through yourresolveUserByCredentials, which must returnPromise<User | null>
Narrowing after check()
Section titled “Narrowing after check()”check() returns Promise<boolean> and user() returns Promise<TUser | null>. They are separate calls because check() is a lightweight session-validity check, while user() calls resolveUser to fetch the full user from your database.
The recommended pattern is to call user() and null-check the result:
const session = auth();const user = await session.user();
if (!user) { return res.status(401).json({ error: 'Not authenticated' });}
// TypeScript narrows `user` to `User` hereconsole.log(user.role);If you only need to know whether the user is authenticated without fetching user data, use check():
const session = auth();
if (!(await session.check())) { return res.status(401).json({ error: 'Not authenticated' });}
// If you also need the user, call user() separatelyconst user = await session.user(); // still User | null — check() doesn't narrow thisTyping resolveUser and resolveUserByCredentials
Section titled “Typing resolveUser and resolveUserByCredentials”Both resolveUser and resolveUserByCredentials must return Promise<TUser | null> matching your generic. TypeScript enforces this at the config level.
Prisma
Section titled “Prisma”import { createAuth, createHash } from 'ideal-auth';import { db } from './db';
interface User { id: string; email: string; name: string; role: string; password: string;}
const hash = createHash();
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, hash, resolveUser: (id) => db.user.findUnique({ where: { id } }), resolveUserByCredentials: (creds) => db.user.findUnique({ where: { email: creds.email } }),});Drizzle
Section titled “Drizzle”import { eq } from 'drizzle-orm';import { createAuth, createHash } from 'ideal-auth';import { db } from './db';import { users } from './schema';
interface User { id: string; email: string; name: string; password: string;}
const hash = createHash();
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, hash, resolveUser: async (id) => { const rows = await db.select().from(users).where(eq(users.id, id)); return (rows[0] as User) ?? null; }, resolveUserByCredentials: async (creds) => { const rows = await db .select() .from(users) .where(eq(users.email, creds.email)); return (rows[0] as User) ?? null; },});Raw SQL
Section titled “Raw SQL”import { createAuth, createHash } from 'ideal-auth';import { pool } from './db';
interface User { id: string; email: string; name: string; password: string;}
const hash = createHash();
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, hash, resolveUser: async (id) => { const { rows } = await pool.query<User>( 'SELECT * FROM users WHERE id = $1', [id], ); return rows[0] ?? null; }, resolveUserByCredentials: async (creds) => { const { rows } = await pool.query<User>( 'SELECT * FROM users WHERE email = $1', [creds.email], ); return rows[0] ?? null; },});Omitting password from the user type
Section titled “Omitting password from the user type”A common pattern is that your database model includes a password field, but you want the User type returned by session.user() to exclude it. There are two approaches.
Approach 1: Omit at the type level
Section titled “Approach 1: Omit at the type level”Define a DBUser type that includes the password and a User type that omits it. Use DBUser internally and User as the generic:
interface DBUser { id: string; email: string; name: string; password: string;}
type User = Omit<DBUser, 'password'>;
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, hash, resolveUser: async (id) => { const row = await db.user.findUnique({ where: { id } }); if (!row) return null; const { password, ...user } = row; return user; }, resolveUserByCredentials: async (creds) => { // This function MUST return the password field for attempt() to work. // ideal-auth reads it via passwordField before stripping it from the session. return db.user.findUnique({ where: { email: creds.email } }) as any; }, passwordField: 'password', // ideal-auth reads this field from the resolved user});Approach 2: Keep password in the type, strip in resolveUser
Section titled “Approach 2: Keep password in the type, strip in resolveUser”A simpler approach is to include password in your type and strip it only in resolveUser (which is what session.user() calls):
interface User { id: string; email: string; name: string; password: string;}
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, hash, resolveUser: async (id) => { const user = await db.user.findUnique({ where: { id }, select: { id: true, email: true, name: true }, // omit password from select }); return user as User | null; }, resolveUserByCredentials: (creds) => db.user.findUnique({ where: { email: creds.email } }),});This way, resolveUserByCredentials returns the full object (including the password hash for attempt()), while resolveUser returns only the fields you want in the session.
Exported types
Section titled “Exported types”ideal-auth exports all of its types so you can use them in your application code. Import them with the type keyword:
import type { AuthConfig, AuthInstance, HashInstance } from 'ideal-auth';Here is the full list:
| Type | Description |
|---|---|
AnyUser | Base user constraint: { id: string | number; [key: string]: any } |
CookieBridge | Interface for the cookie adapter: get, set, delete |
CookieOptions | Full cookie options: httpOnly, secure, sameSite, path, maxAge, expires, domain |
ConfigurableCookieOptions | Omit<CookieOptions, 'httpOnly'> — options you can override in config |
SessionPayload | JWT-like payload stored in the cookie: { uid, iat, exp } |
AuthConfig<TUser> | Configuration object for createAuth |
LoginOptions | Options for login() and attempt(): { remember?: boolean } |
AuthInstance<TUser> | The session object returned by auth() |
HashConfig | Configuration for createHash: { rounds?: number } |
HashInstance | Hash object with make(password) and verify(password, hash) |
TokenVerifierConfig | Configuration for createTokenVerifier: { secret, expiryMs? } |
TokenVerifierInstance | Token verifier with createToken(userId) and verifyToken(token) |
RateLimitStore | Interface for custom rate limit stores: increment and reset |
RateLimiterConfig | Configuration for createRateLimiter: { maxAttempts, windowMs, store? } |
RateLimitResult | Result of attempt(): { allowed, remaining, resetAt } |
TOTPConfig | Configuration for createTOTP: { digits?, period?, window? } |
TOTPInstance | TOTP object with generateSecret, generateQrUri, verify |
RecoveryCodeResult | Result of verifyRecoveryCode: { valid, remaining } |
ConfigurableCookieOptions
Section titled “ConfigurableCookieOptions”ConfigurableCookieOptions is defined as:
type ConfigurableCookieOptions = Omit<CookieOptions, 'httpOnly'>;The httpOnly flag is excluded because ideal-auth always forces httpOnly: true on session cookies. This prevents client-side JavaScript from reading the session cookie, which is a critical XSS protection.
Use ConfigurableCookieOptions when typing session cookie overrides in your auth config:
import type { ConfigurableCookieOptions } from 'ideal-auth';
const cookieOverrides: Partial<ConfigurableCookieOptions> = { secure: true, sameSite: 'strict', domain: '.example.com',};
const auth = createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, session: { cookie: cookieOverrides, }, resolveUser: (id) => db.user.findUnique({ where: { id } }),});You can also use it when building wrappers or utility functions that accept cookie options:
import type { ConfigurableCookieOptions } from 'ideal-auth';
function createAuthWithDefaults( cookieOptions?: Partial<ConfigurableCookieOptions>,) { return createAuth<User>({ secret: process.env.SESSION_SECRET, cookie: cookieBridge, session: { cookie: { secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', ...cookieOptions, }, }, resolveUser: (id) => db.user.findUnique({ where: { id } }), });}