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
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.IDEAL_AUTH_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()
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` here
console.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() separately
const user = await session.user(); // still User | null — check() doesn't narrow thisTyping resolveUser and resolveUserByCredentials
Both resolveUser and resolveUserByCredentials must return Promise<TUser | null> matching your generic. TypeScript enforces this at the config level.
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.IDEAL_AUTH_SECRET!,
cookie: cookieBridge,
hash,
resolveUser: (id) => db.user.findUnique({ where: { id } }),
resolveUserByCredentials: (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
});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.IDEAL_AUTH_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
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.IDEAL_AUTH_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
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
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.IDEAL_AUTH_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
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.IDEAL_AUTH_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
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
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.IDEAL_AUTH_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.IDEAL_AUTH_SECRET!,
cookie: cookieBridge,
session: {
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
...cookieOptions,
},
},
resolveUser: (id) => db.user.findUnique({ where: { id } }),
});
}