Backend Architecture — Express.js TypeScript API¶
Part:
saas-boilerplate/| Type: Backend API Generated: 2026-02-12 | Last Modified: 2026-03-26 | Scan Level: Exhaustive | Workflow: document-project v1.2.0
Executive Summary¶
Production-ready Express.js 4.18 REST API built with TypeScript 5.4 and PostgreSQL via TypeORM 0.3. Implements multi-tenant SaaS patterns including RBAC, subscription management with Stripe/LemonSqueezy, CMS, and admin operations. Designed for serverless deployment (Vercel) with traditional server fallback.
Codebase at a glance: 289 TypeScript files, 30 modules, 29 entities, 212 API endpoints, 14 middleware files, 10 auth policies, 4 auth handlers, 7 event handlers, 2 cron jobs, 4 service factories.
Technology Stack¶
| Category | Technology | Version | Purpose |
|---|---|---|---|
| Runtime | Node.js | 18.17+ | Server runtime (not enforced via engines in package.json) |
| Language | TypeScript | 5.4 | Type safety |
| Framework | Express.js | 4.18 | HTTP server |
| Database | PostgreSQL | 14+ | Primary datastore |
| ORM | TypeORM | 0.3.25 | Database abstraction |
| Validation | Zod | 3.22 | Request validation |
| Auth | JWT (jsonwebtoken) | 9.0 | Token-based auth |
| Auth | Passport + passport-google-oauth20 | 0.7/2.0 | OAuth strategies |
| Passwords | bcrypt | 5.1 | Password hashing (async, non-blocking) |
| Payments | Stripe | 18.3 | Subscription billing |
| Payments | LemonSqueezy | 4.0 | Alternative billing (webhooks only — payment factory not implemented) |
| File Storage | Cloudinary | 2.7 | Image uploads |
| File Storage | AWS S3 | 3.x | Alternative storage |
| File Upload | multer | 2.0 | Multipart form handling |
| Nodemailer | 6.9 | SMTP email | |
| Resend | 6.1 | Transactional email | |
| Brevo (GetBrevo) | 3.0 | Marketing email | |
| Logging | Winston + express-winston | 3.17/4.2 | Structured logging with daily rotate |
| Security | Helmet | 8.1 | HTTP headers |
| Sanitization | DOMPurify (isomorphic-dompurify) | — | Backend HTML sanitization (via sanitizer factory) |
| Sanitization | sanitize-html | 2.17 | XSS prevention (legacy) |
| Compression | compression | 1.8 | Response gzip |
| Rate Limiting | express-rate-limit | 8.0 | Request throttling |
| Scheduling | node-cron | 4.2 | Cron jobs |
| 2FA | speakeasy + qrcode | 2.0/1.5 | TOTP authentication |
| HTTP Client | axios | 1.11 | External API calls |
| Dates | date-fns + luxon | 3.6/3.4 | Date manipulation (date-fns minimally used in coupon service; luxon installed but unused — consider removing both or consolidating) |
| Slugs | slugify | 1.6 | URL slug generation |
| Caching | node-cache | 5.1 | In-memory cache |
| Docs | swagger-jsdoc + swagger-ui-express | 6.2/4.6 | API documentation |
| IDs | uuid | 9.0 | UUID generation |
| DB Driver | pg | 8.16 | PostgreSQL driver |
| Env | dotenv | 16.4 | Environment variable loading |
| HTTP Status | http-status | 1.7 | Status code constants |
Architecture Pattern¶
Modular MVC with Service Layer — Each business domain is isolated into a self-contained module under src/app/modules/v1/:
src/app/modules/v1/{Domain}/
├── {domain}.controller.ts # HTTP request/response handling
├── {domain}.service.ts # Business logic
├── {domain}.routes.ts # Express route definitions (some modules use .route.ts — no consistent convention)
├── {domain}.validation.ts # Zod validation schemas (missing in Image, Public, Purchase; empty in Setting)
└── {domain}.utils.ts # Domain-specific utilities (optional; some modules use .helpers.ts or .repository.ts)
Compliance: 18/30 modules follow this pattern exactly. 10 have partial deviations (missing files or naming inconsistencies). 2 are non-standard: Logger (service-only, no controller/routes) and Swagger (route-only, no controller/service). File naming conventions vary: hyphenated (
generic-page.*), underscored (app_config.*), camelCase (menuItem.*), and plain (blog.*).
Directory Structure¶
saas-boilerplate/
├── src/
│ ├── server.ts # Entry point — HTTP server + DB init (Vercel handler export)
│ ├── app.ts # Express app factory — middleware pipeline
│ ├── config/ # 8 config files
│ │ ├── index.ts # Centralized env config (30+ variable groups)
│ │ ├── AppConfig.ts # Singleton config class with required env validation at startup
│ │ ├── dbConfig.ts # TypeORM DataSource configuration (contains nod_env typo)
│ │ ├── passport.ts # Google OAuth strategy (handles missing email)
│ │ ├── getStripe.ts # Stripe instance factory
│ │ ├── stripe.ts # Direct Stripe instance singleton
│ │ ├── cloudinary.ts # Cloudinary v2 configuration
│ │ └── getLemonsqueezy.ts # LemonSqueezy SDK setup + checkout
│ ├── db/
│ │ └── connection.ts # Serverless-safe DataSource singleton
│ ├── entity/ # 29 TypeORM entities
│ ├── app/
│ │ ├── modules/v1/ # 30 feature modules (see API Modules)
│ │ ├── middlewares/ # 14 middleware files
│ │ │ ├── auth.ts # JWT authentication guard
│ │ │ ├── hasPermission.ts # RBAC permission check (legacy — completely unused, zero imports)
│ │ │ ├── permissionMiddleware.ts # Active RBAC: permission() and hasAnyPermission()
│ │ │ ├── contextMiddleware.ts # AsyncLocalStorage request context + permission cache init
│ │ │ ├── globalErrorHandler.ts# Centralized error handler (Zod, Stripe, ApiError)
│ │ │ ├── rateLimiter.ts # Rate limiting (configurable window/max/message)
│ │ │ ├── requireStripe.ts # Stripe availability guard
│ │ │ ├── upload.ts # Multer file upload (memory storage, image types only)
│ │ │ ├── validateRequest.ts # Zod schema validation (replaces req.body with validated data)
│ │ │ ├── sanitizeQuery.ts # Query parameter type casting (string→number/boolean)
│ │ │ ├── sequrity.ts # [sic] Helmet security headers (CSP, HSTS, Frameguard)
│ │ │ ├── swaggerAuthMiddleware.ts # Swagger docs auth (Basic auth + session)
│ │ │ ├── logger.ts # Winston logging (JSON + PostgreSQL transport)
│ │ │ └── middleware.ts # Middleware registration + error handler setup
│ │ ├── routes/v1/
│ │ │ ├── index.ts # Route aggregator (30 route registrations, all under /api/v1/ prefix)
│ │ │ └── webhook.route.ts # Webhook routes (root-level, before JSON parser)
│ │ ├── events/
│ │ │ ├── eventEmitter.ts # Node.js EventEmitter singleton
│ │ │ ├── eventTypes.ts # Event type constants (10 defined, 7 with handlers)
│ │ │ ├── index.ts # Handler registration
│ │ │ └── handlers/ # 7 event handlers
│ │ ├── cron/
│ │ │ ├── index.ts # Cron initialization
│ │ │ ├── scheduleResetLoginAttempts.ts
│ │ │ └── subscribtionExpire.ts # [sic] should be subscriptionExpire
│ │ ├── errors/ # ApiError class + handleZodError utility
│ │ ├── interfaces/ # 8 TypeScript interface files + sanitizer/ subdirectory
│ │ │ └── sanitizer/
│ │ │ └── sanitizer.interface.ts # ISanitizer interface
│ │ ├── factories/ # Sanitizer factory (DI pattern)
│ │ │ └── sanitizer/
│ │ │ └── sanitizer.factory.ts # Creates ISanitizer instances
│ │ ├── infrastructure/ # DOMPurify sanitizer implementation
│ │ │ └── sanitizer/
│ │ │ └── dompurify.sanitizer.ts # DOMPurify backend sanitizer
│ │ ├── transporter/ # PostgresTransport (Winston DB transport)
│ │ ├── policy/ # Auth validation policies (10 files)
│ │ │ ├── Policy.ts # Base policy class
│ │ │ └── policies/ # 9 concrete policies
│ │ │ ├── LoginAttemptPolicy.ts # Login attempt limit validation
│ │ │ ├── LoginPolicy.ts # Login flow orchestration
│ │ │ ├── OtpPolicy.ts # OTP verification validation
│ │ │ ├── PasswordAuthPolicy.ts # Password authentication
│ │ │ ├── RegistrationPolicy.ts # Registration validation
│ │ │ ├── TokenValidityPolicy.ts # Password reset token validation
│ │ │ ├── TwoFactorPolicy.ts # 2FA validation
│ │ │ ├── UserExistencePolicy.ts # User existence verification
│ │ │ └── UserStatusPolicy.ts # Account status verification
│ │ └── builder/ # Query builders (getQueryBuilder.ts + QueryBuilder.ts)
│ ├── lib/ # Service factories (added alongside legacy shared/ utilities)
│ │ ├── cache/ # Cache abstraction layer
│ │ │ ├── cache.interface.ts # ICache interface
│ │ │ └── Node-cache.ts # NodeCache implementation
│ │ ├── email/ # Email provider factory — 10 files including template/ subdirectory
│ │ │ ├── EmailFactory.ts # Provider selection via MAIL_TYPE config
│ │ │ ├── IEmailService.ts # Email service interface
│ │ │ ├── NodeMailerService.ts # Nodemailer provider
│ │ │ ├── ResendService.ts # Resend provider
│ │ │ ├── BrevoService.ts # Brevo provider
│ │ │ └── ... # + type definitions, transporter, template/
│ │ ├── payment/ # Payment provider factory — 4 files
│ │ │ ├── PaymentFactory.ts # Provider selection (Stripe only — LemonSqueezy declared but NOT implemented)
│ │ │ ├── StripeService.ts # Stripe provider
│ │ │ └── ... # + type definitions, service
│ │ └── storage/ # Storage provider factory — 5 files
│ │ ├── StorageFactory.ts # Provider selection (Cloudinary/S3 only — Local file exists but NOT integrated)
│ │ ├── CloudinaryStorage.ts # Cloudinary provider
│ │ ├── AwsStorage.ts # AWS S3 provider
│ │ ├── LocalSotrage.ts # [sic] Local storage (exists but unused by factory)
│ │ └── IStorageService.ts # Storage interface
│ ├── shared/ # 15 shared utility files (legacy cache.ts still here despite lib/cache/)
│ │ ├── catchAsync.ts # Async error wrapper for RequestHandler
│ │ ├── sendResponse.ts # Standardized JSON response format
│ │ ├── cache.ts # Legacy node-cache wrapper (1hr TTL, prefix-based invalidation)
│ │ ├── permissionCache.ts # Request-scoped permission cache (Set<string>) with ownership validation
│ │ ├── permissionNormalizer.ts # Bidirectional format conversion: /route/action ↔ resource.action
│ │ ├── requestContext.ts # AsyncLocalStorage: RequestContext {user, permissionCache, requestId}
│ │ ├── getCorsOrigins.ts # Parses ALLOWED_ORIGINS env var
│ │ ├── getDataSource.ts # TypeORM DataSource accessor
│ │ ├── getDbRepository.ts # Generic repository factory (by entity class or table name)
│ │ ├── getSortQuery.ts # Dynamic sort builder for queries
│ │ ├── generateUniqueSlug.ts # Slug generator with uniqueness check
│ │ ├── pick.ts # Object property picker utility
│ │ ├── sanitizeImageName.ts # Image filename sanitizer
│ │ ├── normalizeImageSrc.ts # Image URL normalizer
│ │ └── validatePostTypeAndId.ts # Menu post type validator
│ ├── helper/ # 24+ utility files (top-level + cloud/ and email/ subdirectories)
│ │ ├── uploadToCloudinary.ts # Cloudinary upload (auto-converts to WebP)
│ │ └── httpLogger.ts # Winston HTTP logger
│ ├── seed/ # Database seeders (6 files: seed.ts, permission.seed.ts, role.seed.ts, setting.seed.ts + data/permission.ts, data/setting.ts)
│ ├── template/ # 5 Email HTML templates
│ └── public/ # Static files (swagger.yaml)
├── solution docs/ # Applied fix documentation (AUTH, JWT, OTP, HASHED_PASSWORD, CONFIG, PASSPORT)
├── package.json
├── tsconfig.json # ES2016 target, CommonJS modules, strict mode
├── .env.example # 30+ env variable groups
└── CLAUDE.md # AI assistant context file
Middleware Pipeline (Execution Order)¶
Global middleware (registered in middleware.ts → registerMiddlewares()):
Request
├─ 1. securityMiddleware (sequrity.ts) # Helmet: CSP, HSTS, Frameguard, noSniff
├─ 2. cookieParser() # Parse cookies
├─ 3. Request logger # Console log method + path (dev)
├─ 4. compression() # Gzip responses
├─ 5. cors({ credentials: true }) # Dynamic CORS origins from ALLOWED_ORIGINS
├─ 6. express-session # Sessions for OAuth (httpOnly, 30-day max)
├─ 7. passport.initialize/session # OAuth middleware
├─ 8. express.json() + urlencoded() # Body parsers
├─ 9. Request start time tracker # For response time metrics
├─ 10. contextMiddleware # AsyncLocalStorage + permission cache init (global for all routes; also listed per-route because it has dual-path behavior: OAuth requests get full context loading, JWT-authenticated requests may short-circuit)
Route registration (in app.ts):
├─ 11. /webhooks/* (RAW body) # Stripe/LemonSqueezy webhooks (before JSON parse)
├─ 12. GET / (root welcome) # Health check / welcome
├─ 13. express.static('/uploads') # Static file serving
├─ 14. /api/v1/* → router # Main API routes (30 modules)
├─ 15. globalErrorHandler # Zod, Stripe, ApiError error handling
└─ 16. 404 handler # Not found JSON response
Per-route middleware (applied in individual route files):
- auth() — JWT authentication with access token refresh (not rotation — only a new access token is issued, the refresh token is not replaced)
- permission(perm) — Check specific permission from cache
- validateRequest(schema) — Zod validation (handles multipart JSON)
- rateLimiter(opts) — Rate limiting (auth routes only)
- upload.single/array() — Multer file upload (memory storage)
- sanitizeQuery — Query parameter type casting (blog list only)
- requireStripe — Stripe key availability check (subscription routes)
- swaggerAuthMiddleware — Basic auth + session for Swagger docs
API Modules (30 Modules + Webhook) — 212 Endpoints¶
| Path | Module | Endpoints | Description |
|---|---|---|---|
/api/v1/auth |
Auth | 16 | Login, register, refresh, OTP, OAuth, 2FA, password reset |
/api/v1/users |
User | 13 | User CRUD, profile, bulk delete/restore |
/api/v1/analytics |
Analytic | 1 | Dashboard metrics and statistics |
/api/v1/package-categories |
PackageCategory | 5 | Subscription tier categories |
/api/v1/packages |
Package | 9 | Subscription plans CRUD |
/api/v1/roles |
Role | 10 | Role CRUD with permission assignment |
/api/v1/subscriptions |
Subscription | 3 | User subscription lifecycle |
/api/v1/payments |
Payment | 2 | Payment processing records |
/api/v1/purchases |
Purchase | 2 | Purchase history and records |
/api/v1/permissions |
Permission | 15 | Permission CRUD |
/api/v1/coupons |
Cupon [sic] | 5 | Discount coupon management |
/api/v1/app-configs |
AppConfig | 5 | Encrypted app configurations |
/api/v1/user-settings |
UserSetting | 8 | User preferences, 2FA settings |
/api/v1/blogs |
Blog | 11 | Blog post CRUD with SEO, bulk ops |
/api/v1/blog-categories |
BlogCategory | 11 | Hierarchical blog categories |
/api/v1/blog-tags |
BlogTag | 10 | Blog tag management |
/api/v1/products |
Product | 5 | Product catalog CRUD |
/api/v1/product-categories |
ProductCategory | 6 | Product category management |
/api/v1/pages |
GenericPage | 11 | CMS static/dynamic pages |
/api/v1/contacts |
Contact | 10 | Contact form submissions (entity: ContactMessage) |
/api/v1/uploads |
Image | 1 | File upload (Cloudinary/S3 — no entity, operates on files) |
/api/v1/public |
Public | 8 | Public endpoints (no auth — aggregates public data from other entities) |
/api/v1/slugs |
Slug | 1 | URL slug validation/generation (utility — no own entity) |
/api/v1/settings |
Setting | 7 | Site settings (key-value store) |
/api/v1/menus |
MenuBuilder/Menu | 10 | Menu CRUD |
/api/v1/menu-items |
MenuBuilder/MenuItem | 11 | Hierarchical menu items |
/api/v1/invite-users |
InviteUser | 11 | User invitation management |
/api/v1/user-invites |
InviteUser | — | Alias route (same module as invite-users) |
/api/v1/email-templates |
EmailTemplate | 11 | Email template CRUD (JSONB elements) |
/api/v1/docs |
Swagger | 2 | API documentation UI |
/webhooks/* |
Webhook | 2 | Stripe/LemonSqueezy webhooks (root level) |
Total: 212 unique endpoints across 30 modules + webhook.
Note:
Loggermodule exists undermodules/v1/with only alogger.service.ts(no controller or routes, 0 endpoints) — violates the MVC pattern. A loosestripe.service.tsalso exists directly inmodules/v1/outside any module directory — this is an architectural violation of the modular pattern. TheMenuBuildermodule uses a hierarchical structure withMenu/andMenuItem/subdirectories (each containing a full MVC file set), deviating from the flat module convention. Thepackagemodule directory uses lowercase instead of PascalCase like all other modules.
Common CRUD Pattern¶
Many data modules implement (Auth, Public, Slug, Analytic, Swagger, and Logger do not follow this pattern):
- GET / — List with pagination, search, filters, sorting
- GET /:id — Get by ID or slug
- POST / — Create with Zod validation
- PATCH /:id — Update
- DELETE /:id — Soft delete
- PATCH /bulk-delete — Bulk soft delete
- PATCH /bulk-restore — Bulk restore
- PATCH /:id/restore — Single restore
- DELETE /:id/permanent — Hard delete
Database Schema (29 Entities)¶
Note on table names: Entities using explicit
@Entity('table_name')have the documented table name. Entities using bare@Entity()default to the lowercase class name with no underscores (e.g.,PackageCategory→packagecategory, notpackage_category).
User Management¶
| Entity | Table | Key Fields | Relations |
|---|---|---|---|
| User | user |
id, name, email, password, status (active/inactive/suspend), provider (credential/google/facebook/github/otp), avatar, phone, address, stripeCustomerId, lemonSqueezyCustomerId, inviteOnly | ManyToOne→Role (primary), ManyToMany→Role (extra), OneToMany→Purchase, OneToOne→Subscription, OneToOne→UserSetting, OneToMany→Blog |
| Role | role |
id, name, displayName | OneToMany→User, ManyToMany→Permission (via role_permission), ManyToMany→User (via user_roles) |
| Permission | permission |
id, route (unique), displayName, readonly | — |
| UserSetting | user_setting |
id, userId, isTwoFactorEnabled, twoFactorProvider (email/google_authenticator/sms), twoFactorSecret, tempTwoFactorSecret, backupCodes, language | OneToOne→User |
| InviteUser | user_invites |
id, email (unique), status (PENDING/ACCEPTED) | — |
Authentication & Security¶
| Entity | Table | Key Fields |
|---|---|---|
| Token | token |
id, userId, token (SHA-256 hash), expiresAt — stores password reset tokens; created on forgot-password, consumed on reset |
| OtpVerification | otpverification (bare @Entity) |
id, email, optHash [sic], isVerified, expiresAt — file: Otp.ts, class: OtpVerification (filename/class name mismatch) |
| LoginAttempt | loginattempt (bare @Entity) |
id, userId, failedAttemptsCount, lastFailedAt, lockUntil, isBlocked |
| Logger | logger |
id, status, responseTime, success, errorMessage, userId, ip, requestedAt |
Subscription & Payments¶
| Entity | Table | Key Fields | Relations |
|---|---|---|---|
| Package | package |
id, name, slug, price, durationInDays, type, isFree, features (JSON array), billingCycle (trial/monthly/yearly), stripePriceId, lemonVariantId | OneToMany→Subscription |
| PackageCategory | packagecategory (bare @Entity) |
id, name (unique), slug | — |
| Subscription | subscription |
id, userId, packageId, startDate, endDate, status (active/expired/pending/cancelled), stripeSubscriptionId | OneToOne→User, ManyToOne→Package |
| Payment | payment |
id, amount, currency, status (pending/completed/failed/refunded), method (stripe/paypal/lemonsqueezy), referenceId, paymentDate, metadata (JSON) | — |
| Purchase | purchase |
id, packageId, paymentId, couponId, originalPrice, discountAmount, amountPaid, userId | ManyToOne→User, Package, Payment |
| Coupon | coupon |
id, code (unique), discountType (percent/fixed), amountOff, duration (once/repeating/forever), durationInMonths, stripeCouponId, isActive — file: Cupon.ts [sic] |
— |
Content Management¶
| Entity | Table | Key Fields | Relations |
|---|---|---|---|
| Blog | blogs |
id, title, slug (unique), content, excerpt, imageUrl, imageAlt, status (draft/published/archived), publishedAt, authorName, authorId, isFeatured, showOnSlider, isTrending, SEO fields (metaTitle, metaDescription, metaKeywords, robotsIndex, robotsFollow) | ManyToOne→User (author), ManyToMany→BlogCategory (via blog_categories_pivot), OneToMany→BlogTag |
| BlogCategory | blogcategory (bare @Entity) |
id, name, slug, description, serial, parentId | Self-referencing hierarchy, ManyToMany→Blog |
| BlogTag | blog_tags |
id, name, slug, blogId | ManyToOne→Blog |
| BlogViewCount | blog_view_counts |
id, blogId, count | ManyToOne→Blog |
| GenericPage | generic_pages |
id, title, slug, description, image, status, SEO fields | — |
E-Commerce¶
| Entity | Table | Key Fields | Relations |
|---|---|---|---|
| Product | products |
id, name, images (text array), description, categoryId, price, discountPrice, currency | ManyToOne→ProductCategory |
| ProductCategory | productcategory (bare @Entity) |
id, name, description | OneToMany→Product |
Navigation & Configuration¶
| Entity | Table | Key Fields | Relations |
|---|---|---|---|
| Menu | menus |
id, name, slug (unique), locations (JSON array) | OneToMany→MenuItem |
| MenuItem | menu_items |
id, menuId, title, link, linkType (dynamic/static), target, iconClass, color, parentId, order, postType (blogs/pages/categories/tags/packages), postId, parameters, permissions | Self-referencing hierarchy, ManyToOne→Menu |
| Setting | settings |
id, key, display_name, value, details, backupCodes, type, order, group | — |
| AppConfig | app_configs |
id, key (unique), displayName, value, isEncrypted | — |
| EmailTemplate | email_templates |
id, title, type (unique), subject, elements (JSONB), html | — |
| ContactMessage | contact_messages |
id, name, email, details | — |
| UserRole | userrole (bare @Entity) |
userId, roleId, createdAt, updatedAt, deletedAt | ManyToOne→User, ManyToOne→Role — explicit entity for the user_roles junction (file: UserRoles.ts) |
Junction Tables¶
user_roles(userId, roleId) — User ↔ Role many-to-many (also has explicitUserRoleentity above)role_permission(roleId, permissionId) — Role ↔ Permission many-to-manyblog_categories_pivot(blogId, categoryId) — Blog ↔ BlogCategory many-to-many
Database Design Patterns¶
- Soft delete: All 29 entities use
@DeleteDateColumn— recoverable deletion - Compound indexes: Performance-optimized (e.g.,
[status, deletedAt],[email, deletedAt]) - Partial indexes: PostgreSQL WHERE clauses on active records (e.g.,
WHERE "deletedAt" IS NULL) - Enum columns: TypeORM enum types for status/type fields (note: capitalization varies — InviteUser uses UPPER_CASE enums while Subscription/Payment/Blog use lowercase)
- Hierarchical data: Self-referencing
parentIdfor BlogCategory and MenuItem
Authentication Architecture¶
Login Flows¶
- Email/Password → Validate → Check LoginAttempt lockout → Generate JWT + Refresh
- Email/Password + 2FA → Validate → Require OTP → Verify via Email/TOTP → JWT
- OTP Login → Send OTP (crypto.randomInt CSPRNG) → Verify code → Auto-create user if needed → JWT
- Google OAuth → Passport strategy → Express session stores OAuth profile →
google-oauth.controller.tsretrieves session data → generates JWT + Refresh token → redirects to frontend with tokens
Permission System (RBAC)¶
Active system: permissionMiddleware.ts — used for all route protection:
- permission(perm) — checks if req.user.permissions Set contains the required permission string
- hasAnyPermission(perms[]) — checks if user has ANY of multiple permissions
- Relies on contextMiddleware having already initialized the permission cache
Legacy system: hasPermission.ts — completely unused (zero imports anywhere in codebase). Retained in the codebase but applied to no routes:
- Normalizes request path (removes /api/v1 prefix)
- Maps HTTP methods: GET→read, POST→create, PUT/PATCH→edit, DELETE→delete
- Constructs permission string: /{resource}/{action} (e.g., /users/create)
- Queries Role with permissions from database on each request (no caching)
Permission flow:
1. auth() middleware authenticates user and attaches req.user
2. contextMiddleware loads full user with all roles and permissions, initializes PermissionCache (Setpermission() or hasAnyPermission() checks against the cached Set
Permission format: Supports both route format (/users/create) and dot notation (users.create) via permissionNormalizer.ts
- Super admin bypasses all permission checks
- Users can have primary role (
roleId) + additional roles viauser_rolesjunction table
Sanitizer Architecture (DI Pattern)¶
A clean dependency-injection pattern for HTML sanitization:
src/app/interfaces/sanitizer/sanitizer.interface.ts # ISanitizer interface
src/app/infrastructure/sanitizer/dompurify.sanitizer.ts # DOMPurify implementation
src/app/factories/sanitizer/sanitizer.factory.ts # Factory creating ISanitizer instances
This replaces ad-hoc sanitize-html usage with a testable, swappable backend sanitizer using isomorphic-dompurify. Consumers call the factory to get an ISanitizer instance without knowing the concrete implementation.
Event System¶
Event-driven via Node.js EventEmitter (eventEmitter.ts) for decoupled side effects:
| Event Type Constant | Handler File | Action |
|---|---|---|
SEND_LOGIN_OTP |
sendLoginOtp.handler.ts | Send OTP email |
FORGOT_PASSWORD |
sendForgotPass.handler.ts | Send reset email |
SEND_REGISTRATION_EMAIL |
registerEmailHandler.ts | Send verification email |
INVITE_USER_CREATED |
inviteUserCreate.handler.ts | Send invitation email |
INVITEONLY_USER_LOGIN |
inviteOnlyLoginSendEmail.ts | Handle invite-only login |
RESET_LOGIN_ATTEMPTS |
resetLoginAttempts.handler.ts | Clear failed attempts (>7 days old) |
RESET_SUBSCRIPTION_STATUS |
resetSubscribtionStatus.handler.ts [sic] | Mark ACTIVE subs with past endDate as EXPIRED |
Dead event types (defined in eventTypes.ts but no handlers registered):
- OTP_VERIFIED — no handler
- RESET_PASSWORD — no handler
- VERIFY_EMAIL_REG — no handler
Cron Jobs¶
| Job | Purpose |
|---|---|
scheduleResetLoginAttempts |
Reset failed login attempt counters |
subscribtionExpire [sic] |
Check and expire overdue subscriptions |
Webhook Integration¶
Registered at root level (before JSON parser for raw body signature verification):
Stripe Events: checkout.session.completed, invoice.payment_succeeded, payment_intent.payment_failed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted
LemonSqueezy Events: order_created, order_updated, subscription_created, subscription_updated, subscription_cancelled
Service Factories (src/lib/)¶
Service factories were added in src/lib/ alongside (not replacing) legacy utilities in src/shared/:
| Factory | Location | Providers | Config Key |
|---|---|---|---|
| Cache | lib/cache/ |
NodeCache (ICache interface) | — |
lib/email/ |
Nodemailer, Resend, Brevo | MAIL_TYPE |
|
| Payment | lib/payment/ |
Stripe only (LemonSqueezy declared in type signature but NOT implemented — will throw at runtime) | — |
| Storage | lib/storage/ |
Cloudinary, AWS S3 only (LocalSotrage.ts [sic] exists but NOT wired into StorageFactory) | IMAGE_STORAGE |
Auth Policy System (src/app/policy/)¶
The auth module uses a policy-based validation pattern with a base Policy class and 9 concrete policies:
| Policy | Purpose |
|---|---|
LoginAttemptPolicy |
Validates login attempt limits and lockout |
LoginPolicy |
Orchestrates login flow validation |
OtpPolicy |
Validates OTP verification attempts |
PasswordAuthPolicy |
Handles password authentication checks |
RegistrationPolicy |
Validates registration requirements |
TokenValidityPolicy |
Validates password reset tokens |
TwoFactorPolicy |
Validates 2FA authentication flow |
UserExistencePolicy |
Verifies user existence with configurable settings |
UserStatusPolicy |
Verifies user account status for authentication |
Auth Module Structure (Refactored)¶
The Auth module (src/app/modules/v1/Auth/) was refactored with handler extraction and cleanup completed:
Auth/
├── auth.controller.ts # Main controller (typo fixed: was auth.contoroller.ts)
├── auth.service.ts # Main service with policy-based validation
├── auth.route.ts # Route definitions (createAccount renamed to requestRegistration)
├── auth.utils.ts # Auth utilities (setTokenCookie with corrected maxAge)
├── auth.validation.ts # Zod validation schemas (password min-length updated)
├── google-oauth.controller.ts # OAuth-specific controller (extra controller outside standard pattern)
└── handlers/ # Extracted auth handlers
├── LoginAttemptHandler.ts # Login attempt tracking/lockout
├── OtpHandler.ts # OTP generation (crypto.randomInt CSPRNG) and verification
├── TokenHandler.ts # JWT generation and refresh token expiration
└── UserHandler.ts # User account operations + OTP password handling (async bcrypt)
Note: Cleanup completed Mar 8-11. All backup/refactored files and
auth.helpers.tshave been removed. The controller typo (auth.contoroller.ts) has been fixed.
Environment Configuration (30+ Properties)¶
Database, JWT (4 secrets with expiration), Email (SMTP/Resend/Brevo), Payments (Stripe + LemonSqueezy + legacy SSL), Storage (Cloudinary + AWS S3), OAuth (Google + Facebook placeholders), Admin seed credentials, CORS origins, Master key, Session secret. See .env.example for full reference.
Known config typos: nod_env (should be node_env) in dbConfig.ts, sesseion_secret (should be session_secret).
Startup validation (via
AppConfig.tssingleton): Required env vars (JWT_SECRET,REFRESH_TOKEN_SECRET,DB_URL,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,SESSION_SECRET) are validated at startup — missing vars throw immediately. The singleton pattern ensures validation runs exactly once.Resolved typo:
auth.contoroller.tswas renamed toauth.controller.tson 2026-03-08.
Recent Changes (2026-03-12 through 2026-03-26)¶
Security and architecture improvements across 8 commits:
| Change | Impact |
|---|---|
OTP generation uses crypto.randomInt (CSPRNG) |
Replaces insecure Math.random — OTPs are now cryptographically random |
| Password hashing is async (non-blocking bcrypt) | Prevents event loop blocking during hash computation |
JWT decodeToken validates signature before decoding |
Prevents accepting tampered tokens |
AppConfig.ts singleton with required env validation |
Fails fast on missing config; singleton ensures one-time validation |
| Google OAuth handles missing email gracefully | Prevents crash when Google profile lacks email |
| Sanitizer factory + DOMPurify infrastructure added | Clean DI pattern for backend HTML sanitization |
| Auth handler extraction (4 handlers) | Login, OTP, Token, User logic extracted from monolithic controller |
| 6 solution docs added | Documented fixes for AUTH, JWT, OTP, HASHED_PASSWORD, CONFIG, PASSPORT |
Known Filename Typos¶
| File | Issue |
|---|---|
sequrity.ts |
Should be security.ts |
subscribtionExpire.ts |
Should be subscriptionExpire.ts |
resetSubscribtionStatus.handler.ts |
Should be resetSubscriptionStatus.handler.ts |
LocalSotrage.ts |
Should be LocalStorage.ts |
Cupon.ts (entity) / Cupon/ (module) |
Should be Coupon |
Otp.ts (entity file) |
Class inside is OtpVerification — filename/class name mismatch |
Known Architectural Issues¶
| Issue | Severity | Details |
|---|---|---|
hasPermission.ts completely unused |
Low | Zero imports across entire codebase; permissionMiddleware.ts is the active RBAC system |
sequrity.ts filename typo |
Low | Should be security.ts; functional but confusing |
subscribtionExpire.ts filename typo |
Low | Should be subscriptionExpire.ts |
nod_env typo in dbConfig.ts |
Medium | May cause incorrect environment detection |
| Logger module has no routes | Low | Only logger.service.ts — violates MVC pattern |
Loose stripe.service.ts in modules/v1/ |
Low | File exists outside any module directory |
| LemonSqueezy payment factory not implemented | Medium | Declared in type signature but throws at runtime |
LocalSotrage.ts not wired into StorageFactory |
Low | File exists but factory ignores it |
Legacy shared/cache.ts coexists with lib/cache/ |
Low | Duplicate cache abstraction |
Development Commands¶
yarn dev # Hot reload via ts-node-dev (port 5500)
yarn seed # Run database seeders
yarn build # Compile TypeScript to dist/
yarn start # Run compiled production server
yarn lint # ESLint
Generated by BMAD Document Project workflow v1.2.0 — Exhaustive scan, 2026-02-12 | Last modified: 2026-03-26