Architecture Overview#
AuthHero follows a modular, layered architecture designed for maintainability and extensibility. This page explains the project structure, design patterns, and data flow.
Project Structure#
src/
├── index.ts # Library entry point (public API surface)
├── createAuthHero.ts # Factory function — initializes everything
├── server.ts # Standalone server with graceful shutdown
├── app.ts # Express app setup (middleware + routes)
│
├── config/ # ⚙️ Configuration layer
│ ├── env.ts # Zod-validated environment variables
│ ├── jwt.ts # JWT generation & verification
│ ├── constants.ts # Token lengths, expiry durations
│ ├── cookies.ts # Cookie options (httpOnly, secure)
│ ├── cors.ts # CORS with origin whitelist
│ ├── http.ts # HTTP status code constants
│ ├── prisma.ts # PrismaClient initialization
│ ├── redis.ts # Redis client + BullMQ connection
│ └── email.ts # Nodemailer transporter
│
├── lib/ # 📚 Shared libraries
│ ├── AppError.ts # Custom error class + error codes
│ ├── asyncHandler.ts # Express async error wrapper
│ ├── logger.ts # Pino structured logger
│ ├── session.ts # Centralized session creation
│ └── queues/
│ └── email.queue.ts # BullMQ email queue
│
├── middlewares/ # 🛡️ Express middlewares
│ ├── auth.middleware.ts # JWT authentication
│ ├── mfa.middleware.ts # MFA enforcement
│ ├── error.middleware.ts # Global error handler
│ ├── validate.middleware.ts # Zod validation factory
│ └── rateLimiter.middleware.ts
│
├── modules/ # 📦 Feature modules
│ └── auth/
│ ├── auth.service.ts # Core auth business logic
│ ├── auth.controller.ts # Express route handlers
│ ├── auth.routes.ts # Route definitions
│ ├── auth.validation.ts # Zod request schemas
│ ├── auth.types.ts # TypeScript interfaces
│ ├── mfa/ # MFA sub-module
│ │ ├── mfa.crypto.ts
│ │ ├── mfa.service.ts
│ │ ├── mfa.controller.ts
│ │ ├── mfa.routes.ts
│ │ └── mfa.validation.ts
│ └── oauth/ # OAuth sub-module
│ ├── oauth.service.ts
│ ├── oauth.controller.ts
│ ├── oauth.routes.ts
│ ├── oauth.types.ts
│ └── providers/
│ ├── google.provider.ts
│ ├── github.provider.ts
│ └── facebook.provider.ts
│
├── utils/ # 🔧 Utility functions
│ ├── hash.ts # Argon2 password hashing
│ ├── email.ts # Email sending with HTML template
│ ├── rateLimiter.ts # Redis-backed rate limiter factory
│ └── requireAuth.ts # TypeScript assertion helper
│
└── workers/
└── email.worker.ts # BullMQ email workerLayered Architecture#
Every feature module follows a consistent 5-layer pattern. This separation ensures each layer has a single responsibility:
Request Flow: Client → Route → Controller → Service → Database
auth.routes.tsDefine HTTP methods, paths, and middleware chain (rate limiter → validation → auth → handler).
auth.validation.tsZod schemas validate request bodies before they reach the controller. Invalid data never touches business logic.
auth.controller.tsExtract data from req, call the service, format the response. No business logic here.
auth.service.tsAll business logic lives here — database queries, token generation, email sending, error handling.
auth.types.tsTypeScript interfaces for request payloads, response shapes, and Express type augmentation.
Design Patterns#
Factory Pattern — createAuthHero()#
The main entry point is a factory function that initializes all dependencies (database, Redis, email worker) through dynamic imports and returns a clean public API:
export async function createAuthHero(): Promise<AuthHero> {
// Dynamic imports — nothing runs until you call this function.
const { default: app } = await import("./app");
const { prisma } = await import("./config/prisma");
const { redisClient } = await import("./config/redis");
const { emailWorker } = await import("./workers/email.worker");
const { authenticate } = await import("./middlewares/auth.middleware");
const { requireMFA } = await import("./middlewares/mfa.middleware");
// ...
return {
app, // Full Express app
routes: { auth, oauth, mfa }, // Individual routers
authenticate, // JWT middleware
requireMFA, // MFA enforcement middleware
errorMiddleware, // Global error handler
prisma, // Database client
shutdown, // Graceful cleanup
};
}Why dynamic imports? Nothing executes until you call createAuthHero(). This means environment validation, database connections, and Redis connections are lazy — they only happen when explicitly triggered.
Strategy Pattern — OAuth Providers#
Each OAuth provider implements the same OAuthProvider interface. The service doesn't know provider-specific details — it calls getProfile(code) and gets a standardized user profile:
interface OAuthProvider {
getProfile(code: string): Promise<OAuthUserProfile>;
}
interface OAuthUserProfile {
providerUserId: string;
email: string;
fullname: string;
provider: string;
}class OAuthService {
private static providers: Record<string, OAuthProvider> = {
google: new GoogleProvider(),
github: new GitHubProvider(),
facebook: new FacebookProvider(),
};
static async handleCallback(providerName: string, code: string) {
const strategy = this.providers[providerName];
const profile = await strategy.getProfile(code);
// ... sync with database
}
}Adding a new provider is just three steps:
- Create a new class implementing
OAuthProvider - Register it in the
providersmap - Add the provider's env vars to
env.ts
Single Source of Truth — Session Creation#
Sessions are created in exactly one place: lib/session.ts. This function is used by:
- Email/password login
- MFA challenge completion
- OAuth callback
export async function createSession(
userId: string,
userAgent?: string,
ipAddress?: string,
): Promise<SessionTokens> {
const refreshToken = generateRandomToken(TOKEN_LENGTH.REFRESH);
const refreshTokenHash = hashRandomToken(refreshToken);
const expiresAt = addDays(new Date(), TOKEN_EXPIRY.REFRESH_TOKEN_DAYS);
const session = await prisma.session.create({
data: { userId, refreshTokenHash, expiresAt, userAgent, ipAddress },
});
const accessToken = generateAccessToken(userId, session.id);
return { accessToken, refreshToken };
}Centralizing this prevents drift between flows and ensures every session gets the same security properties.
Dual-Mode Architecture#
AuthHero can be used in two fundamentally different ways:
Mode 1: Standalone Server#
Use the built-in Express app with everything pre-configured — Helmet, CORS, cookie parsing, rate limiting, all auth routes, and a global error handler:
import "dotenv/config";
import { createAuthHero } from "@nandalalshukla/auth-hero";
const auth = await createAuthHero();
auth.app.listen(3000);Mode 2: Library (Mount on Your App)#
Already have an Express app? Mount only the route modules you need:
import "dotenv/config";
import express from "express";
import cookieParser from "cookie-parser";
import { createAuthHero } from "@nandalalshukla/auth-hero";
const app = express();
app.use(express.json());
app.use(cookieParser());
const auth = await createAuthHero();
// Mount auth routes
app.use("/auth", auth.routes.auth);
app.use("/auth/oauth", auth.routes.oauth);
app.use("/auth/mfa", auth.routes.mfa);
// Your own routes
app.get("/dashboard", auth.authenticate, (req, res) => {
res.json({ userId: req.user!.userId });
});
// Error handler MUST be last
app.use(auth.errorMiddleware);
app.listen(3000);Middleware Pipeline#
Every request flows through a carefully ordered middleware chain. The order matters:
1. trust proxy 1 → Real client IP behind reverse proxy
2. helmet() → Security headers (CSP, HSTS, X-Content-Type-Options)
3. express.json() → Parse JSON bodies (16kb limit)
4. cookieParser() → Parse cookies (refresh token, OAuth state)
5. cors() → CORS with strict origin whitelist
6. Routes → Auth, OAuth, MFA route handlers
7. errorMiddleware → Catches all errors, returns structured JSONPer-route middleware is applied in the route definitions:
POST /auth/login
→ loginRateLimiter (5 requests/minute per IP+email)
→ validate(loginSchema) (Zod body validation)
→ asyncHandler(loginController)
POST /auth/mfa/challenge
→ mfaChallengeRateLimiter (5 requests/minute)
→ validate(challengeMFASchema)
→ asyncHandler(challengeMFA)
GET /auth/me
→ authenticate (JWT verification + session DB check)
→ asyncHandler(meController)Async Email Architecture#
Email sending is decoupled from the HTTP request-response cycle using BullMQ (Redis-backed job queue):
HTTP Request
→ Controller adds job to emailQueue (BullMQ)
→ Returns response immediately (fast)
Email Worker (separate process)
→ Picks up job from queue
→ Sends email via Nodemailer/SMTP
→ Retries on failure (BullMQ built-in)Benefits:
- API responses are fast regardless of SMTP server speed
- Consistent response times prevent timing-based email enumeration
- Failed emails are automatically retried
- Email worker can be scaled independently
The worker handles sendVerificationEmail, resendVerificationEmail, and sendPasswordResetEmail. Each uses the same HTML email template wrapper for consistent branding.
Token Architecture#
| Token | Type | Lifetime | Storage |
|---|---|---|---|
| Access Token | JWT (signed) | 15 minutes | Client memory / Authorization header |
| Refresh Token | Random (80 hex chars) | 30 days | HTTP-only secure cookie + DB (SHA-256 hash) |
| Email Verification | Random (72 hex chars) | 10 minutes | DB (SHA-256 hash) |
| Password Reset | Random (72 hex chars) | 15 minutes | DB (SHA-256 hash) |
| MFA Temp Token | JWT (signed) | 5 minutes | Client (returned from login) |
| OAuth One-Time Code | Random (64 hex chars) | 120 seconds | Redis (auto-expires) |
Every time a refresh token is used, a new one is issued and the old one becomes invalid. If a revoked token is reused (indicating theft), all sessions for that user are immediately revoked.