Skip to content

Service Factories (Email, Payment, Storage, Cache) - Deep Dive Documentation

Generated: 2026-03-29 Scope: saas-boilerplate/src/lib/{email,payment,storage,cache}/ + config + helper consumers Files Analyzed: 45+ (20 lib factory files + 16 config/consumer files + helper layer) Lines of Code: ~1,400+ (factory core) + ~800+ (helper/config layer) Workflow Mode: Exhaustive Deep-Dive Issues Found: ~147 unique (28 Critical, 33 High, 46 Medium, 40 Low) + 6 systemic

Overview

Four factory-pattern subsystems in src/lib/ provide abstracted interfaces for external service integration: email delivery (3 providers), payment processing (Stripe + LemonSqueezy), file storage (AWS S3, Cloudinary, Local), and caching (node-cache). Each follows a Strategy Pattern with interface + concrete implementations + factory selector.

The central discovery: These factories represent an incomplete refactoring. Only ~11% of consumers use the factory abstractions; ~89% use legacy helper/config systems that predate them. Two entire subsystems (storage, cache) are 100% dead code with zero consumers.

Purpose: Decouple business logic from third-party service providers via the Strategy Pattern Key Responsibilities: Provider selection, configuration, SDK initialization, request/response normalization Integration Points: SMTP/Resend/Brevo APIs, Stripe/LemonSqueezy SDKs, AWS S3/Cloudinary/Local filesystem, node-cache in-memory store

Factory Adoption & Production Readiness

Factory Consumers Using Factory Consumers Using Legacy Adoption % Production Ready
Email 2 4 33% ~40% (broken by default)
Payment 1 ~10 ~9% ~15% (hardcoded "stripe")
Storage 0 6 0% 0% (entire dir dead code)
Cache 0 5 0% 0% (entire dir dead code)

Competing Systems Inventory

Subsystem # Systems Implementations
Email 3 lib/email/EmailFactory, helper/email.ts (legacy MAIL_TYPE), lib/email/email.ts (dead)
Payment 4 lib/payment/PaymentFactory, config/stripe.ts (sync), config/getStripe.ts (async+DB), config/getLemonsqueezy.ts
Storage 3 lib/storage/StorageFactory (dead), helper/handleFileUpload.ts chain, config/cloudinary.ts
Cache 3 lib/cache/Node-cache.ts (dead), shared/cache.ts (production), shared/permissionCache.ts (AsyncLocalStorage)
Total 13

Complete File Inventory

Email Factory (lib/email/ — 9 files, ~157 LOC)


src/lib/email/IEmailService.ts

Purpose: Defines the contract interface that all email providers must implement. Single method sendEmail with IEmailSendPayload. Lines of Code: ~8 File Type: Interface definition

What Future Contributors Must Know: This is the abstraction layer — any new email provider must implement this interface. However, the interface is minimal (no batch, CC/BCC, attachments, or error contract).

Exports: - interface IEmailService { sendEmail(payload: IEmailSendPayload): Promise<void> } — Provider contract - interface IEmailSendPayload { to: string; subject: string; html: string } — Email payload type

Dependencies: - None (standalone interface)

Used By: - EmailFactory.ts (return type) - NodeMailerService.ts (implements) - ResendService.ts (implements) - BrevoService.ts (implements — but missing explicit implements keyword)

Key Implementation Details: - to is string only — no array support for multiple recipients - No from field — each provider handles sender differently - No attachment support (helper version has it) - No error contract — callers cannot distinguish failure modes

Patterns Used: - Strategy Pattern: Interface for dependency inversion

Side Effects: None (pure type definition)

Error Handling: No error contract defined — each provider handles errors differently

Testing: No test files exist - Coverage: 0%


src/lib/email/EmailFactory.ts

Purpose: Factory that selects an email provider based on EMAIL_SERVICE env var. Returns singleton instances of NodeMailer, Resend, or Brevo services. Lines of Code: ~28 File Type: Factory / Provider selector

What Future Contributors Must Know: This factory is broken by defaultEMAIL_SERVICE is not in .env or .env.example, so it always throws "Unknown email service type: undefined". Only 2 consumers use this (registration + invite handlers), and both silently fail.

Exports: - class EmailFactory { static getEmailService(): IEmailService } — Singleton factory method

Dependencies: - IEmailService from ./IEmailService - NodeMailerService from ./NodeMailerService - ResendService from ./ResendService - BrevoService from ./BrevoService - config from ../../config (reads config.mail.service)

Used By: - app/events/handlers/registerEmailHandler.ts — Registration verification emails - app/events/handlers/inviteUserCreate.handler.ts — Invite emails

Key Implementation Details:

static getEmailService(): IEmailService {
  const emailServiceType = config.mail.service; // EMAIL_SERVICE env var
  console.log("Email service type:", emailServiceType); // Logs on every call
  switch (emailServiceType) {
    case "nodemailer": return new NodeMailerService();
    case "resend": return new ResendService();
    case "brevo": return new BrevoService();
    default: throw new Error(`Unknown email service type: ${emailServiceType}`);
  }
}

Side Effects: - console.log on every factory call (leaks config in production) - Reads config from environment

Error Handling: Throws on unknown/undefined provider — crashes caller

Issues: - F1 [CRITICAL]: EMAIL_SERVICE missing from .env and .env.example — factory always throws - F2 [MEDIUM]: console.log on every call - F3 [LOW]: No runtime type narrowing — arbitrary strings passed through - Comment says "storage service" — copy-pasted from StorageFactory


src/lib/email/NodeMailerService.ts

Purpose: SMTP email provider using Nodemailer. Creates a transporter, verifies SMTP connection, then sends email. Lines of Code: ~35 File Type: Service implementation

What Future Contributors Must Know: Uses explicit new Promise(async ...) anti-pattern that can cause unhandled rejections. Verifies SMTP on every send (latency hit). Hardcodes from address with "Sass" typo and example.com domain. Disables TLS verification (rejectUnauthorized: false).

Exports: - class NodeMailerService implements IEmailService { sendEmail(payload): Promise<void> } — SMTP sender

Dependencies: - nodemailer — SMTP library - IEmailService, IEmailSendPayload from ./IEmailService - config from ../../config

Used By: - EmailFactory.ts (instantiated when EMAIL_SERVICE=nodemailer)

Key Implementation Details: - Creates new transporter per send (no connection pooling) - Calls transporter.verify() before every send — adds latency - Hardcoded from: "Sass Template" <[email protected]> (typo + placeholder domain) - rejectUnauthorized: false disables TLS cert verification

Side Effects: - SMTP connection attempt on every send - console.log("Email sent successfully") on success

Issues: - N1 [HIGH]: rejectUnauthorized: false enables MITM attacks - N2 [HIGH]: new Promise(async (resolve, reject) => ...) anti-pattern - N3 [MEDIUM]: Hardcoded from address ("Sass" typo, example.com) - N4 [LOW]: transPorter inconsistent casing - N5 [LOW]: verify() on every send adds latency


src/lib/email/ResendService.ts

Purpose: Email provider using Resend HTTP API. Sends transactional emails via Resend SDK. Lines of Code: ~18 File Type: Service implementation

What Future Contributors Must Know: Zero error handling — all Resend SDK errors propagate as unhandled. Uses SMTP user email (MAIL_USER) as from address instead of Resend verified domain. Unsafe as string cast on config.

Exports: - class ResendService implements IEmailService { sendEmail(payload): Promise<void> } — Resend API sender

Dependencies: - resend — Resend SDK - IEmailService, IEmailSendPayload from ./IEmailService - config from ../../config

Used By: - EmailFactory.ts (instantiated when EMAIL_SERVICE=resend)

Key Implementation Details:

const resend = new Resend(config.mail.pass as string); // RESEND_API_KEY via MAIL_PASS
await resend.emails.send({
  from: config.mail.user as string, // SMTP user, not Resend domain
  to: payload.to,
  subject: payload.subject,
  html: payload.html,
});

Issues: - R1 [HIGH]: Unsafe as string cast — sends undefined if MAIL_USER unset - R2 [MEDIUM]: Zero error handling - R3 [MEDIUM]: Wrong from address source (SMTP user instead of Resend verified domain) - R4 [LOW]: Redundant html spread - R5 [LOW]: No attachment support (helper version has it)


src/lib/email/BrevoService.ts

Purpose: Email provider using Brevo (formerly Sendinblue) API. Sends transactional emails via Brevo SDK. Lines of Code: ~45 File Type: Service implementation

What Future Contributors Must Know: Silently swallows all errors — catch block logs but doesn't rethrow. sendEmail() resolves even on failure, so callers believe the email was sent. Contains hardcoded test email object that is never used. Sets API key on every send instead of constructor.

Exports: - class BrevoService implements IEmailService { sendEmail(payload): Promise<void> } — Brevo API sender

Dependencies: - @getbrevo/brevo — Brevo SDK - IEmailService, IEmailSendPayload from ./IEmailService - config from ../../config

Used By: - EmailFactory.ts (instantiated when EMAIL_SERVICE=brevo)

Key Implementation Details:

async sendEmail(payload: IEmailSendPayload): Promise<void> {
  try {
    apiInstance.setApiKey(/*...*/); // Re-sets on every call
    const sendSmtpEmail = new brevo.SendSmtpEmail();
    // ... configure email ...
    await apiInstance.sendTransacEmail(sendSmtpEmail);
  } catch (error) {
    console.error("Error sending email:", error); // Logs but does NOT rethrow
    // Caller thinks email was sent successfully
  }
}

Issues: - B1 [CRITICAL]: Silent error swallowing — sendEmail resolves on failure - B2 [HIGH]: Hardcoded test email object (lines 25-33) with "Hello from Brevo SDK" — dead code - B3 [MEDIUM]: Unused imports (CreateContact, ContactsApi) - B4 [MEDIUM]: setApiKey on every send instead of constructor - B5 [HIGH]: Unsafe as string cast on config values - B6 [LOW]: Hardcoded sender name "Your App" in dead test object - B7 [LOW]: BREVO_API_KEY absent from all env files


src/lib/email/email.ts — DEAD CODE

Purpose: Alternative email sender function with 'use server' directive (React Server Actions — meaningless in Express.js backend). Lines of Code: ~33 File Type: Dead standalone function

What Future Contributors Must Know: This file has ZERO importers. Contains a Next.js 'use server' directive in an Express.js backend. Creates its own nodemailer transporter separate from NodeMailerService. Hardcodes "Sass" from address with example.com domain. In dev mode, silently returns success without sending.

Exports: - async function sendEmail(payload: IEmailSendPayload): Promise<void> — Standalone email sender

Dependencies: - nodemailer - Local IEmailSendPayload interface (duplicated, not imported from IEmailService.ts) - config

Used By: Nothing — DEAD CODE

Issues: - E1 [HIGH]: 'use server' directive in Express.js backend - E2 [HIGH]: Dev mode silently swallows failures (returns success) - E3 [MEDIUM]: Duplicates IEmailSendPayload locally - E4 [MEDIUM]: Hardcoded "Sass" typo and example.com - E5 [LOW]: verify() on every send


src/lib/email/transporterMail.ts — DEAD CODE

Purpose: Creates and exports a nodemailer transporter instance. Lines of Code: ~18 File Type: Dead configuration module

What Future Contributors Must Know: Only imported by dead email.ts — transitively dead. Creates a separate transporter from NodeMailerService with rejectUnauthorized: false. Contains commented-out CommonJS export.

Exports: - const transporter — Nodemailer transporter instance

Dependencies: - nodemailer - config

Used By: Only lib/email/email.ts (which is also dead)

Issues: - TM1 [HIGH]: rejectUnauthorized: false (MITM risk — same as NodeMailerService) - TM2 [MEDIUM]: Duplicate transporter creation (separate from NodeMailerService) - TM3 [LOW]: Dead CommonJS export comment // module.exports = transporter - TM4 [LOW]: Transitively dead — only consumer is dead email.ts


src/lib/email/template/signupEmailVerification.ts — DEAD CODE

Purpose: Generates an HTML email template for signup email verification with an embedded verification link. Lines of Code: ~13 File Type: Dead template function

What Future Contributors Must Know: Zero importers anywhere in codebase. All email templates used in production are in src/template/ (different directory). Contains a pointless ${"there"} template literal expression and interpolates verifyUrl without URL escaping (XSS risk if URL contains user input).

Exports: - function signupEmailVerification(verifyUrl: string): string — HTML template generator

Dependencies: None

Used By: Nothing — DEAD CODE

Issues: - ST1 [CRITICAL]: Zero importers — completely dead code - ST2 [MEDIUM]: XSS risk — verifyUrl not escaped - ST3 [LOW]: ${"there"} pointless template expression - ST4 [LOW]: Only 1 template in folder (no reset, OTP, invite templates)


Payment Factory (lib/payment/ — 4 files, ~180 LOC)


src/lib/payment/payment.type.ts

Purpose: Type definitions for the payment abstraction layer. Defines interfaces for payment options, verification results, and the payment service contract. Lines of Code: ~25 File Type: Type definitions

What Future Contributors Must Know: The IPaymentService interface is extremely thin — only createCheckout, verify, and createBillingPortal. No refund, cancel, customer management, or subscription operations. The amount field in IPaymentOptions is never consumed by any implementation.

Exports: - interface IPaymentOptions { planId: string | number; customerId: string; amount: number; currency: string; metadata: Record<string, unknown> } — Checkout options - interface IPaymentVerifyResult { success: boolean; data: any } — Verification result - interface IPaymentService { createCheckout(options): Promise<{checkoutUrl, sessionId}>; verify(sessionId): Promise<IPaymentVerifyResult>; createBillingPortal(customerId): Promise<{url}> } — Provider contract - type PaymentProvider = "stripe" | "lemonsqueezy" — Provider union type

Dependencies: None

Used By: - PaymentFactory.ts, StripeService.ts, paymentService.ts

Issues: - #1 [MEDIUM]: amount field never consumed by any implementation - #2 [LOW]: planId: string | number — number branch misleading for Stripe - #3 [LOW]: metadata: Record<string, unknown> — Stripe expects Record<string, string> - #4 [MEDIUM]: Interface too thin — no refund, cancel, customer, or validate methods - #5 [LOW]: verify returns any for data — eliminates type safety


src/lib/payment/PaymentFactory.ts

Purpose: Factory that creates payment service instances based on provider name. Currently only supports Stripe despite accepting "lemonsqueezy" in type. Lines of Code: ~18 File Type: Factory / Provider selector

What Future Contributors Must Know: Accepts "lemonsqueezy" in the TypeScript type but throws at runtime because there's no case for it. No singleton/caching — creates new StripeService per call. Only 1 consumer (paymentService.ts) and it hardcodes "stripe" anyway.

Exports: - class PaymentFactory { static create(provider: PaymentProvider): IPaymentService } — Factory method

Dependencies: - StripeService from ./StripeService - PaymentProvider from ./payment.type

Used By: - app/modules/v1/payment.service.ts (hardcodes PaymentFactory.create("stripe"))

Key Implementation Details:

static create(provider: PaymentProvider): IPaymentService {
  switch (provider) {
    case "stripe": return new StripeService();
    // NO case for "lemonsqueezy" — throws default
    default: throw new Error("Unsupported payment provider");
  }
}

Issues: - #6 [CRITICAL]: "lemonsqueezy" accepted by TypeScript but throws at runtime - #7 [HIGH]: No singleton — new StripeService per call - #8 [MEDIUM]: Factory doesn't read provider from config — caller must hardcode


src/lib/payment/StripeService.ts

Purpose: Stripe implementation of IPaymentService. Creates checkout sessions, verifies payments, and manages billing portal. Lines of Code: ~108 File Type: Service implementation

What Future Contributors Must Know: Uses stripe?. optional chaining that returns undefined instead of throwing when Stripe is null. The cancelUrl uses {CHECKOUT_SESSION_ID} template — Stripe only replaces this in success_url, not cancel_url, so cancel redirects to a 404. Has methods (createCheckoutSession, isValidPriceId) that leak outside the IPaymentService interface.

Exports: - class StripeService implements IPaymentService — Stripe payment provider - createCheckout(options: IPaymentOptions): Promise<{checkoutUrl, sessionId}> - verify(sessionId: string): Promise<IPaymentVerifyResult> - createBillingPortal(customerId: string): Promise<{url}> - createCheckoutSession(priceId, customerId, metadata) — NOT on interface - isValidPriceId(priceId) — NOT on interface

Dependencies: - stripe from stripe SDK - config from ../../config - Types from ./payment.type

Used By: - PaymentFactory.ts (instantiated)

Key Implementation Details:

constructor() {
  this.stripe = config.stripe.secretKey
    ? new Stripe(config.stripe.secretKey)
    : null; // Null if no key
}
// Then uses this.stripe?.checkout.sessions.create(...)
// Returns { checkoutUrl: undefined, sessionId: undefined } when null!

Side Effects: - Stripe API calls (checkout session create, session retrieve, billing portal create) - Reads CLIENT_URL env var for redirect URLs

Issues: - #9 [CRITICAL]: cancelUrl uses {CHECKOUT_SESSION_ID} — not replaced by Stripe in cancel_url - #10 [CRITICAL]: Optional chaining returns { checkoutUrl: undefined } silently instead of throwing - #11 [HIGH]: Methods leak outside IPaymentService interface - #12 [MEDIUM]: verify() silently returns { success: false } when Stripe is null - #13 [MEDIUM]: No idempotency keys — retries create duplicate sessions - #14 [LOW]: products: any[] untyped parameter


src/lib/payment/paymentService.ts

Purpose: High-level payment orchestration that uses PaymentFactory to create checkout sessions, verify payments, and manage billing portals. Lines of Code: ~29 File Type: Business logic orchestrator

What Future Contributors Must Know: Hardcodes PaymentFactory.create("stripe") — the paymentGateway parameter passed by callers is completely ignored. This makes the entire multi-provider architecture non-functional.

Exports: - createPaymentForSubscription(paymentGateway, options): Promise<{checkoutUrl, sessionId}> — Create checkout - verifyPayment(paymentGateway, sessionId): Promise<IPaymentVerifyResult> — Verify payment - createBillingPortal(customerId): Promise<{url}> — Create portal

Dependencies: - PaymentFactory from ./PaymentFactory - Types from ./payment.type

Used By: - app/modules/v1/Payment/payment.controller.ts

Issues: - #32 [CRITICAL]: paymentGateway parameter completely ignored — hardcoded "stripe" - #33 [HIGH]: Dead createStripePaymentIntent function (33 LOC, references "paypal") - #34/#35 [HIGH]: Dead imports trigger SDK evaluation


config/stripe.ts (Sync Stripe init)

  • Creates Stripe instance synchronously from STRIPE_SECRET env var
  • Still imported by 5 files despite being superseded by getStripe.ts
  • Issue #15 [HIGH]: Dual Stripe initialization paths

config/getStripe.ts (Async Stripe init with DB fallback)

  • Creates Stripe from env var, falls back to AppConfig DB (AES-256-GCM decrypted)
  • Only factory with encrypted DB secret support
  • Issue #17 [HIGH]: Circular dependency with stripe.service.ts
  • Issue #18 [HIGH]: No cache invalidation — requires process restart after key rotation
  • Issue #19 [MEDIUM]: Decryption crash not caught

config/getLemonsqueezy.ts (LemonSqueezy init)

  • Issue #21 [CRITICAL]: Hardcoded localhost:5500 redirect URL
  • Issue #22 [CRITICAL]: user_id set to email instead of userId
  • Issue #39 [CRITICAL]: Hardcoded webhook secret "c3b451346788c32" in source

modules/v1/stripe.service.ts (Business Stripe ops — naming collision)

  • 261 LOC, 5 consumers — the "real" Stripe service used by most of the codebase
  • Exports object literal (not class) — different from lib/payment/StripeService.ts (class)
  • Issue #26 [CRITICAL]: References stripeProductId column that doesn't exist on Package entity
  • Issue #27 [HIGH]: isDeleted filter on entity using @DeleteDateColumn (different mechanism)
  • Issue #28 [HIGH]: Billing portal config created on every call

Storage Factory (lib/storage/ — 5 files, ~250 LOC) — 100% DEAD CODE


src/lib/storage/IStorageService.ts

Purpose: Interface contract for storage providers. Defines upload, delete, commit, and rollback methods. Lines of Code: ~10 File Type: Interface definition — DEAD CODE

What Future Contributors Must Know: Zero consumers. The commit() and rollback() methods are never implemented by any provider. The key field is optional in upload return but required for delete().

Exports: - interface IStorageService { upload(file): Promise<{url, key?}>; delete(key): Promise<void>; commit?(): Promise<void>; rollback?(): Promise<void> } — Storage contract

Issues: - #1 [MEDIUM]: key optional in return, required for delete() - #2 [LOW]: commit()/rollback() never implemented - #3 [LOW]: No download/exists methods


src/lib/storage/StorageFactory.ts

Purpose: Factory that selects storage provider based on IMAGE_STORAGE config. Returns singleton instance. Lines of Code: ~27 File Type: Factory — DEAD CODE

What Future Contributors Must Know: Zero consumers anywhere. Crashes on the default config value "local" because there is no case "local" in the switch. Only handles "aws" and "cloudinary". Does not import LocalSotrage.ts at all.

Exports: - function getImageStorageService(): IStorageService — Factory function (not class-based like other factories)

Issues: - #4 [CRITICAL]: Default config "local" crashes — no case in switch - #5 [CRITICAL]: Zero consumers — entire factory unused - #6 [HIGH]: No LocalSotrage import — cannot add local support - #7 [MEDIUM]: Singleton not resettable (no way to change provider in tests) - #8 [LOW]: Function name getImageStorageService vs file name StorageFactory


src/lib/storage/AwsStorage.ts

Purpose: AWS S3 storage provider using @aws-sdk/client-s3. Lines of Code: ~89 File Type: Service implementation — DEAD CODE

What Future Contributors Must Know: Pointless base64 round-trip: converts buffer to base64 string then back to buffer before uploading. No filename sanitization — special characters in S3 keys. Public URL assumes bucket is public (no ACL set). No error handling or retry logic.

Exports: - class AwsStorage implements IStorageService — S3 upload/delete

Issues: - #9 [HIGH]: Pointless buffer→base64→buffer round-trip - #10 [HIGH]: Mixed config sources (constructor vs process.env) - #11 [HIGH]: No filename sanitization - #12 [MEDIUM]: No file size/type validation - #13 [MEDIUM]: URL assumes public bucket - #14 [MEDIUM]: No error handling/retry - #15 [MEDIUM]: No concurrency limit on batch upload - #16 [LOW]: Key collision risk (Date.now() millisecond resolution) - #17 [LOW]: No content disposition header


src/lib/storage/CloudinaryStorage.ts

Purpose: Cloudinary storage provider using Cloudinary SDK's upload stream. Lines of Code: ~74 File Type: Service implementation — DEAD CODE

What Future Contributors Must Know: Missing explicit implements IStorageService — no compile-time contract enforcement. Sends base64 string through upload_stream instead of binary buffer, making files ~33% larger. delete() ignores Cloudinary response.

Exports: - class CloudinaryStorage — Cloudinary upload/delete (missing implements IStorageService)

Issues: - #18 [CRITICAL]: Missing implements IStorageService - #19 [HIGH]: Base64 string to upload_stream instead of buffer - #20 [MEDIUM]: Hardcoded "uploads" folder - #21 [MEDIUM]: delete() ignores result - #22 [MEDIUM]: No file type validation - #23 [LOW]: Unnecessary non-null assertions - #24 [LOW]: No concurrency limit on batch


src/lib/storage/LocalSotrage.ts (Filename Typo)

Purpose: Local filesystem storage provider. Writes uploaded files to local disk. Lines of Code: ~50 File Type: Service implementation — DEAD CODE

What Future Contributors Must Know: Filename typo "Sotrage" — never imported anywhere (not even by the factory). upload() writes base64 TEXT to disk instead of binary — every saved file is corrupted/unusable. Path traversal vulnerability via unsanitized file.originalname. saveAfterUpload() has swapped arguments (folderName passed as fileName).

Exports: - class LocalStorage implements IStorageService — Local filesystem upload/delete

Issues: - #25 [CRITICAL]: Filename typo "LocalSotrage.ts" - #26 [CRITICAL]: Writes base64 text, not binary — files corrupted - #27 [CRITICAL]: Path traversal via unsanitized file.originalname - #28 [HIGH]: saveAfterUploads() ignores fileName param - #29 [CRITICAL]: saveAfterUpload() swaps arguments - #30 [MEDIUM]: No directory creation before write - #31 [MEDIUM]: fs.unlinkSync blocks event loop in async method - #32 [MEDIUM]: No overwrite protection - #33 [LOW]: saveAfterUpload/saveAfterUploads not on interface - #34 [LOW]: Partial fs usage


Actual Storage System (Helper Layer — In Production)

The real storage system uses helper/handleFileUpload.tshelper/uploadImage.tshelper/uploadToCloudinary.ts / helper/cloud/s3.ts.

Consumers (5 modules): Blog, User, Setting, GenericPage, Image

Key Issues in Helper Layer: - #35 [CRITICAL]: Two independent upload systems share NO code - #36 [HIGH]: handleFileUpload in "local" mode returns URL but does NOT write file - #37 [HIGH]: Three different S3 implementations with different key formats - #38 [HIGH]: Inconsistent storage type names ("s3" in helper vs "aws" in factory) - #39 [MEDIUM]: Force-converts all uploads to WebP (non-image files broken) - #40 [HIGH]: Hardcoded image/jpeg MIME type in uploadToCloudinary - #41 [MEDIUM]: uploadImage only supports Cloudinary despite generic name - #42 [MEDIUM]: Dead imports in image.service.ts


Cache (lib/cache/ — 2 files, ~37 LOC) — 100% DEAD CODE


src/lib/cache/cache.interface.ts

Purpose: Interface for cache operations. Defines get/set/del/delByPrefix/has/flush contract. Lines of Code: ~5 File Type: Interface definition — DEAD CODE

Exports: - interface ICache { get<T>(key): Promise<T|undefined>; set(key, value, ttl?): Promise<void>; del(key): Promise<void>; delByPrefix(prefix): Promise<void>; has(key): Promise<boolean>; flush(): Promise<void> } — Cache contract

Issues: - H1 [HIGH]: delByPrefix() is most-used method in production cache but NOT on this interface - L10 [LOW]: All methods async for synchronous node-cache ops (over-engineering for Redis swap)


src/lib/cache/Node-cache.ts

Purpose: Class-based node-cache implementation following the ICache interface. Abandoned migration from the functional shared/cache.ts. Lines of Code: ~32 File Type: Service implementation — DEAD CODE

What Future Contributors Must Know: Zero consumers. A git branch cache-v1.0.0 suggests this was an abandoned migration. Uses useClones: false (unlike production shared/cache.ts which uses default true) — allows cache mutation bugs.

Exports: - class NodeCacheService implements ICache — OOP cache wrapper

Issues: - C1 [CRITICAL]: Entirely dead code — zero consumers - C3 [CRITICAL]: Different useClones behavior than production cache - C5 [CRITICAL]: Dual cache infrastructure with no migration path


Production Cache System (shared/cache.ts)

Lines of Code: ~85 Used By: Role service (8 ops), MenuItem service (9 ops), Analytic service (import only — dead)

Key Issues: - C2 [CRITICAL]: Comment says "default TTL: 5 minutes" but code sets 3600s (1 hour) - C4 [CRITICAL]: TTL 60 * 24 = 1440s (24 min) with comments saying "cache for 1 day" — off by 60x - H2 [HIGH]: No maxKeys limit — unbounded memory growth - H3 [HIGH]: JSON double-serialization (stringify + useClones deep-clone) - H4 [HIGH]: No cross-request permission caching — every auth request hits DB - H5 [HIGH]: Wrong file path in header comment - H6 [HIGH]: useClones not explicitly set — relies on library default - M2 [MEDIUM]: revalidateCache is non-atomic (del+set brief miss window) - M3 [MEDIUM]: addPermissionToCache allows runtime injection - M7 [MEDIUM]: No cache warming strategy — cold-start penalty - L1 [LOW]: validateCacheOwnership returns true for "no cache" — misleading - L4 [LOW]: set() uses if (ttlSeconds) — falsy for TTL of 0


Contributor Checklist

Risks & Gotchas

  • Email registration is silently broken in default configuration (EMAIL_SERVICE unset)
  • Real SMTP credentials are committed in .env (Gmail app password)
  • Brevo never reports failures — emails silently drop
  • Payment always uses Stripe regardless of paymentGateway param
  • LemonSqueezy webhook secret is hardcoded in source code
  • Entire lib/storage/ and lib/cache/ are dead code — do not try to "fix" them without migration plan
  • cancelUrl in Stripe is broken — cancel redirects to 404
  • 3 separate S3 implementations with different behaviors
  • TTL comments are lies — 5 min says 1 hour, 1 day means 24 minutes

Pre-change Verification Steps

  1. Check which email system a handler uses before modifying (factory vs helper)
  2. Search for EMAIL_SERVICE and MAIL_TYPE to understand which system is active
  3. Verify Stripe SDK calls against latest Stripe docs (SDK version pinned)
  4. Test cancel URL behavior with actual Stripe checkout
  5. Check IMAGE_STORAGE value before assuming which upload path runs
  6. Verify cache TTLs match documented behavior (read code, not comments)

Suggested Tests Before PR

  • Unit: Each email provider send (mock SMTP/API, verify payload shape)
  • Unit: EmailFactory provider selection (each case + unknown + undefined)
  • Unit: PaymentFactory with mock Stripe (checkout, verify, portal)
  • Unit: StripeService null-Stripe path (should throw, not return undefined)
  • Integration: End-to-end registration email (factory → provider → delivery)
  • Integration: Stripe checkout flow (create → redirect → verify → record)
  • Unit: Cache TTL behavior (set with TTL, verify expiry timing)
  • Unit: Cache key collision test (concurrent sets with Date.now keys)

Architecture & Design Patterns

Code Organization

src/lib/
├── email/                          # 3 providers (NodeMailer, Resend, Brevo)
│   ├── IEmailService.ts            # Interface
│   ├── EmailFactory.ts             # Factory (broken by default)
│   ├── NodeMailerService.ts        # SMTP provider
│   ├── ResendService.ts            # HTTP API provider
│   ├── BrevoService.ts             # HTTP API provider (silent errors)
│   ├── email.ts                    # DEAD CODE
│   ├── transporterMail.ts          # DEAD CODE
│   └── template/
│       └── signupEmailVerification.ts  # DEAD CODE
├── payment/                        # 1 provider (Stripe only)
│   ├── payment.type.ts             # Types + interface
│   ├── PaymentFactory.ts           # Factory (hardcoded "stripe" by caller)
│   ├── StripeService.ts            # Stripe implementation
│   └── paymentService.ts           # Business orchestrator
├── storage/                        # ENTIRELY DEAD CODE
│   ├── IStorageService.ts          # Interface
│   ├── StorageFactory.ts           # Factory (crashes on default config)
│   ├── AwsStorage.ts               # S3 provider
│   ├── CloudinaryStorage.ts        # Cloudinary provider (missing implements)
│   └── LocalSotrage.ts             # Local provider (filename typo, writes corrupted files)
└── cache/                          # ENTIRELY DEAD CODE
    ├── cache.interface.ts           # Interface
    └── Node-cache.ts               # node-cache wrapper

Design Patterns

  • Strategy Pattern: All 4 subsystems define an interface + multiple implementations. However, consistency varies — Email uses class-based factory, Payment uses class-based factory, Storage uses function-based factory, Cache has no factory.
  • Singleton Pattern: Email and Storage factories cache instances; Payment does not (new per call).
  • Factory Method: EmailFactory and PaymentFactory use static create/getEmailService methods. StorageFactory uses a module-scoped function.

Naming Inconsistencies

Aspect Email Payment Storage Cache
Interface file IEmailService.ts Inside payment.type.ts IStorageService.ts cache.interface.ts
Factory file EmailFactory.ts PaymentFactory.ts StorageFactory.ts None
Factory style Static class method Static class method Module function N/A
Provider naming XxxService StripeService XxxStorage NodeCacheService
Singleton Yes No Yes N/A

Error Handling Philosophy

Inconsistent across all 4 subsystems: - NodeMailerService: Catches and rejects via Promise - ResendService: No try/catch — raw propagation - BrevoService: Catches and swallows (CRITICAL) - StripeService: Optional chaining returns undefined (CRITICAL) - StorageFactory: Throws on unknown provider - Cache: No error handling at all

Testing Strategy

ZERO test coverage across all 4 factory subsystems. No test files, no test dependencies, no test scripts.


Data Flow

Email Data Flow

Event Handler (register/invite)
  → EmailFactory.getEmailService() ← BROKEN (EMAIL_SERVICE missing)
    → NodeMailerService.sendEmail() → SMTP server
    → ResendService.sendEmail() → Resend HTTP API
    → BrevoService.sendEmail() → Brevo API (errors swallowed)

Legacy path (helper/email.ts) ← WORKING
  → Event Handler (forgot-pass/login-otp/invite-only)
    → sendEmail() → nodemailer transporter → SMTP server

Payment Data Flow

payment.controller.ts
  → paymentService.createPaymentForSubscription(paymentGateway, options)
    → PaymentFactory.create("stripe") ← paymentGateway IGNORED
      → StripeService.createCheckout()
        → stripe.checkout.sessions.create() → Stripe API
          → redirect to success_url / cancel_url (cancel_url BROKEN)

Webhook path:
  → webhook.controller.ts
    → stripe.webhooks.constructEvent() (signature verification)
    → stripe.service.ts handlers (business operations)
      → getStripe() → AppConfig DB (encrypted) or env var

Storage Data Flow (Production — Helper Layer)

Controller (Blog/User/Setting/GenericPage/Product)
  → handleFileUpload(file, storageType)
    → if "cloudinary": uploadImage() → uploadToCloudinary() → Cloudinary API
    → if "s3": cloud/s3.ts → AWS S3 API
    → if "local": returns URL only (DOES NOT write file)
      → Controller must call saveBufferToFile() manually

Cache Data Flow

Role service → shared/cache.ts (set/get/del with TTL)
MenuItem service → shared/cache.ts (set/get/del with TTL)
contextMiddleware → shared/permissionCache.ts (AsyncLocalStorage, per-request, no TTL)

Integration Points

External APIs Consumed

API Provider Used By Auth Method
SMTP (port 587/465) Gmail/custom NodeMailerService, helper/email.ts MAIL_USER + MAIL_PASS
Resend API Resend ResendService RESEND_API_KEY (via MAIL_PASS)
Brevo API Brevo BrevoService BREVO_API_KEY
Stripe API Stripe StripeService, stripe.service.ts STRIPE_SECRET or AppConfig DB
LemonSqueezy API LemonSqueezy getLemonsqueezy.ts LEMONSQUEEZY_API_KEY
AWS S3 Amazon AwsStorage (dead), helper/cloud/s3.ts AWS_ACCESS_KEY_ID + SECRET
Cloudinary API Cloudinary CloudinaryStorage (dead), helper/uploadToCloudinary CLOUDINARY_* keys

Env Var Map (Complete)

Env Var Used By Present in .env Present in .env.example Notes
EMAIL_SERVICE EmailFactory NO NO MISSING — breaks factory
MAIL_TYPE helper/email.ts NO YES Legacy email system selector
MAIL_HOST NodeMailerService, transporterMail YES YES SMTP host
MAIL_PORT NodeMailerService, transporterMail YES YES SMTP port
MAIL_USER NodeMailerService, ResendService, BrevoService YES YES SMTP user / API from
MAIL_PASS NodeMailerService, ResendService YES YES SMTP pass / API key
MAIL_FROM NOBODY YES NO Configured but unused
RESEND_API_KEY helper/email/resend.ts NO YES Not used by factory
BREVO_API_KEY BrevoService NO NO Missing everywhere
STRIPE_SECRET config/stripe.ts YES YES Primary Stripe key
STRIPE_PUBLISHED_KEY Frontend YES YES Client-side key
STRIPE_LOCAL_WEBHOOK_SECRET webhook.controller.ts YES NO Dev webhook verify
STRIPE_PROD_WEBHOOK_SECRET webhook.controller.ts YES NO Prod webhook verify
LEMONSQUEEZY_API_KEY getLemonsqueezy.ts YES YES LS API key
LEMONSQUEEZY_STORE_ID getLemonsqueezy.ts YES YES Checked but unused
LEMONSQUEEZY_WEBHOOK_SECRET REPLACED N/A N/A Hardcoded in source
IMAGE_STORAGE StorageFactory (dead), handleFileUpload YES YES Default: "local"
AWS_ACCESS_KEY_ID AwsStorage (dead), cloud/s3.ts YES YES S3 auth
AWS_SECRET_ACCESS_KEY AwsStorage (dead), cloud/s3.ts YES YES S3 auth
AWS_REGION AwsStorage (dead), cloud/s3.ts YES YES S3 region
AWS_BUCKET_NAME AwsStorage (dead), cloud/s3.ts YES YES S3 bucket
CLOUDINARY_CLOUD_NAME CloudinaryStorage (dead), config/cloudinary.ts YES YES Cloudinary auth
CLOUDINARY_API_KEY CloudinaryStorage (dead), config/cloudinary.ts YES YES Cloudinary auth
CLOUDINARY_API_SECRET CloudinaryStorage (dead), config/cloudinary.ts YES YES Cloudinary auth
MASTER_KEY config/getStripe.ts YES YES AES-256-GCM encryption key
CLIENT_URL StripeService, getLemonsqueezy YES YES Redirect URLs

AppConfig Relationship

Only Stripe has DB-stored encrypted secret support: - config/getStripe.tsAppConfigService.getConfigByKey("stripe_secret_key") → AES-256-GCM decrypt with MASTER_KEY - All other secrets (SMTP password, Resend key, Brevo key, Cloudinary secret, AWS secret, LemonSqueezy key) are env-var-only, plaintext


Dependency Graph

Entry Points (Not Imported by Others in Scope)

  • EmailFactory.ts (called by event handlers)
  • paymentService.ts (called by payment controller)
  • StorageFactory.ts (DEAD — no callers)
  • Node-cache.ts (DEAD — no callers)

Leaf Nodes (Don't Import Others in Scope)

  • IEmailService.ts
  • payment.type.ts
  • IStorageService.ts
  • cache.interface.ts
  • template/signupEmailVerification.ts

Circular Dependencies

  • config/getStripe.tsmodules/v1/stripe.service.ts — Fragile circular import at startup

Testing Analysis

Test Coverage Summary

  • Statements: 0%
  • Branches: 0%
  • Functions: 0%
  • Lines: 0%

Test Files

None exist for any factory subsystem.

Testing Gaps

  • No unit tests for any email provider
  • No unit tests for factory selection logic
  • No integration tests for email delivery
  • No unit tests for Stripe checkout/verify/portal flows
  • No tests for null-Stripe error paths
  • No tests for webhook signature verification
  • No tests for storage upload/download/delete
  • No tests for cache set/get/del/TTL behavior
  • No tests for concurrent cache access
  • No tests for cache key collision

Duplicate Implementations

Functionality Factory Version Legacy Version Status
Send email lib/email/EmailFactory helper/email.ts Both in production, different config keys
Stripe init lib/payment/StripeService constructor config/stripe.ts, config/getStripe.ts 3 separate init paths
S3 upload lib/storage/AwsStorage helper/cloud/s3.ts Factory dead, helper in production
Cloudinary upload lib/storage/CloudinaryStorage helper/uploadToCloudinary.ts Factory dead, helper in production
Local file write lib/storage/LocalSotrage helper/saveBufferToFile.ts Factory dead (and broken), helper in production
Cache get/set lib/cache/Node-cache.ts shared/cache.ts Factory dead, shared in production

Consolidation Opportunities

  1. Email: Unify to single factory with MAIL_TYPE/EMAIL_SERVICE merger. Remove dead email.ts, transporterMail.ts, signupEmailVerification.ts. Fix BrevoService error handling.
  2. Payment: Complete LemonSqueezy implementation or remove type. Fix paymentGateway passthrough. Consolidate triple Stripe init to single path.
  3. Storage: Either migrate helpers to factory pattern or delete entire lib/storage/. Fix handleFileUpload local mode.
  4. Cache: Either migrate shared/cache.ts to lib/cache/ pattern or delete lib/cache/. Fix TTL mismatches.

Dead Code Summary

File LOC Subsystem Reason
lib/email/email.ts ~33 Email Zero importers
lib/email/transporterMail.ts ~18 Email Only imported by dead email.ts
lib/email/template/signupEmailVerification.ts ~13 Email Zero importers
lib/storage/StorageFactory.ts ~27 Storage Zero consumers
lib/storage/IStorageService.ts ~10 Storage Only used by dead factory
lib/storage/AwsStorage.ts ~89 Storage Only used by dead factory
lib/storage/CloudinaryStorage.ts ~74 Storage Only used by dead factory
lib/storage/LocalSotrage.ts ~50 Storage Never imported (typo)
lib/cache/cache.interface.ts ~5 Cache Zero consumers
lib/cache/Node-cache.ts ~32 Cache Zero consumers
createStripePaymentIntent ~33 Payment Never called (references "paypal")
Various dead imports ~50+ Payment Trigger unnecessary SDK evaluation
TOTAL ~434+

Known Issues — Complete Catalog

Critical (28)

# ID Subsystem Title
1 E-F1 Email EMAIL_SERVICE missing from .env — factory always throws
2 E-B1 Email BrevoService silently swallows errors
3 E-ST1 Email signupEmailVerification.ts zero importers (dead code)
4 E-D1 Email Split-brain email: factory (EMAIL_SERVICE) vs helper (MAIL_TYPE)
5 E-D2 Email Registration/invite emails broken by default
6 E-D3 Email Real SMTP credentials committed to .env
7 E-D4 Email MAIL_FROM configured but unused by all providers
8 P-6 Payment "lemonsqueezy" accepted by TypeScript but throws at runtime
9 P-9 Payment cancelUrl uses {CHECKOUT_SESSION_ID} — Stripe doesn't replace in cancel_url
10 P-10 Payment Optional chaining returns undefined URL instead of throwing
11 P-21 Payment Hardcoded localhost:5500 in LemonSqueezy redirect
12 P-22 Payment LemonSqueezy user_id set to email instead of userId
13 P-26 Payment stripeProductId column doesn't exist on Package entity
14 P-32 Payment paymentGateway parameter completely ignored (hardcoded "stripe")
15 P-39 Payment Hardcoded LemonSqueezy webhook secret in source code
16 S-4 Storage Factory crashes on default config "local" — no case in switch
17 S-5 Storage Entire lib/storage/ is dead code (zero consumers)
18 S-18 Storage CloudinaryStorage missing implements IStorageService
19 S-25 Storage Filename typo "LocalSotrage.ts"
20 S-26 Storage LocalStorage.upload() writes base64 text not binary — files corrupted
21 S-27 Storage Path traversal vulnerability in LocalStorage
22 S-29 Storage saveAfterUpload() swaps arguments (folderName↔fileName)
23 S-35 Storage Two independent upload systems share NO code
24 CA-C1 Cache lib/cache/ entirely dead code (zero consumers)
25 CA-C2 Cache Comment says "5 minutes" but code is 3600s (1 hour)
26 CA-C3 Cache Two NodeCache instances with different useClones behavior
27 CA-C4 Cache TTL 60×24=1440s (24 min) but comments say "1 day" — off by 60x
28 CA-C5 Cache Dual cache infrastructure with no migration path

High (33)

# ID Subsystem Title
1 E-N1 Email TLS cert validation disabled (rejectUnauthorized: false)
2 E-TM1 Email TLS cert validation disabled (transporterMail)
3 E-N2 Email Explicit Promise(async) anti-pattern
4 E-E1 Email 'use server' directive in Express backend
5 E-E2 Email Dev mode silently swallows email failures
6 E-R1 Email Unsafe as string cast in ResendService
7 E-B2 Email Dead test email object in BrevoService
8 E-B5 Email Unsafe as string cast in BrevoService
9 P-7 Payment No singleton/caching in PaymentFactory
10 P-11 Payment Methods leak outside IPaymentService interface
11 P-15 Payment Dual Stripe initialization paths
12 P-17 Payment Circular dependency getStripe ↔ stripe.service
13 P-18 Payment No cache invalidation for Stripe key rotation
14 P-23 Payment LemonSqueezy not in PaymentFactory
15 P-27 Payment isDeleted filter on @DeleteDateColumn entity
16 P-28 Payment Billing portal config created every call
17 P-33 Payment Dead createStripePaymentIntent (33 LOC, "paypal" reference)
18 P-34/35 Payment Dead imports trigger SDK evaluation
19 P-40 Payment Stripe error response leaks error object
20 S-6 Storage No LocalSotrage import in factory
21 S-9 Storage Pointless base64 round-trip in AwsStorage
22 S-10 Storage Mixed config sources in AwsStorage
23 S-11 Storage No filename sanitization in AwsStorage
24 S-19 Storage Base64 string to upload_stream in Cloudinary
25 S-28 Storage saveAfterUploads() ignores fileName param
26 S-36 Storage handleFileUpload "local" doesn't write file
27 S-37 Storage Three different S3 implementations
28 S-38 Storage Inconsistent storage type names ("s3" vs "aws")
29 S-40 Storage Hardcoded image/jpeg MIME type
30 CA-H1 Cache ICache missing delByPrefix() (most-used method)
31 CA-H2 Cache No maxKeys limit — unbounded memory growth
32 CA-H3 Cache JSON double-serialization (stringify + useClones deep-clone)
33 CA-H4 Cache No cross-request permission caching — every auth hits DB

Medium (46)

# ID Subsystem Title
1 E-N3 Email Hardcoded from address ("Sass" typo, example.com)
2 E-E3 Email Duplicate IEmailSendPayload definition
3 E-E4 Email Hardcoded "Sass" from in email.ts
4 E-R2 Email Zero error handling in ResendService
5 E-R3 Email Wrong from address source (SMTP user for Resend)
6 E-B3 Email Unused imports in BrevoService
7 E-B4 Email setApiKey on every send
8 E-I1 Email No error contract on IEmailService
9 E-ST2 Email XSS risk in template (verifyUrl not escaped)
10 E-F2 Email console.log on every factory call
11 P-1 Payment amount field never consumed
12 P-4 Payment Interface too thin (no refund/cancel/customer)
13 P-8 Payment Factory doesn't read from config
14 P-12 Payment verify() silently returns false when null
15 P-13 Payment No idempotency keys
16 P-16 Payment config/stripe.ts still imported by 5 files
17 P-19 Payment Decryption crash not caught
18 P-24 Payment storeId/webhookSecret validated but unused
19 P-25 Payment LemonSqueezy SDK re-initialized every call
20 P-29 Payment validPriceId dead code
21 P-30 Payment Dead stripe import in stripe.service.ts
22 P-36 Payment createPayament typo
23 P-37 Payment Metadata string-to-number inconsistent defaults
24 P-41 Payment Duplicate timingSafeEqual check
25 S-1 Storage key optional in return, required for delete()
26 S-7 Storage Singleton not resettable for tests
27 S-12 Storage No file size/type validation (AwsStorage)
28 S-13 Storage URL assumes public S3 bucket
29 S-14 Storage No error handling/retry for S3
30 S-15 Storage No concurrency limit on batch (AWS)
31 S-20 Storage Hardcoded "uploads" folder (Cloudinary)
32 S-21 Storage delete() ignores result (Cloudinary)
33 S-22 Storage No file type validation (Cloudinary)
34 S-30 Storage No directory creation before write (Local)
35 S-31 Storage fs.unlinkSync blocks event loop
36 S-32 Storage No overwrite protection (Local)
37 S-39 Storage Inconsistent storage type names ("s3" vs "aws")
38 S-41 Storage Force-converts all uploads to WebP
39 S-42 Storage uploadImage only supports Cloudinary
40 CA-M2 Cache revalidateCache non-atomic (del+set miss window)
41 CA-M3 Cache addPermissionToCache allows runtime injection
42 CA-M4 Cache Analytic service dead cache import
43 CA-M5 Cache getRoleList type mismatch
44 CA-M6 Cache getMenuItemList type mismatch
45 CA-M7 Cache No cache warming strategy
46 E-TM2 Email Duplicate transporterMail.ts creation

Low (40)

# ID Subsystem Title
1 E-T1 Email Single recipient only (no array/CC/BCC)
2 E-T2 Email No from field in payload type
3 E-N4 Email transPorter casing inconsistency
4 E-N5 Email verify() on every send (latency)
5 E-R4 Email Redundant html spread
6 E-R5 Email No attachment support (factory)
7 E-ST3 Email ${"there"} pointless template expression
8 E-ST4 Email Only 1 template in folder
9 E-TM3 Email Dead CommonJS export comment
10 E-TM4 Email Transitively dead file
11 E-I2 Email No batch/attachment/CC/BCC in interface
12 E-F3 Email No runtime type narrowing in factory
13 E-B6 Email Hardcoded sender "Your App" in dead code
14 E-B7 Email BREVO_API_KEY absent from env files
15 P-2 Payment planId number branch misleading for Stripe
16 P-3 Payment metadata typed as Record
17 P-5 Payment verify return uses any for data
18 P-14 Payment products: any[] untyped
19 P-20 Payment Console output uses emoji
20 P-31 Payment Mixed function declaration styles
21 P-38 Payment Unnecessary as number cast
22 P-42 Payment Dead import of sync stripe in webhook
23 S-2 Storage commit()/rollback() never implemented
24 S-3 Storage No download/exists methods
25 S-8 Storage Function vs file name mismatch (StorageFactory)
26 S-16 Storage Key collision risk (Date.now())
27 S-17 Storage No content disposition on S3 uploads
28 S-23 Storage Unnecessary non-null assertions (Cloudinary)
29 S-24 Storage No concurrency limit on batch (Cloudinary)
30 S-33 Storage saveAfterUpload/saveAfterUploads not on interface
31 S-34 Storage Partial fs usage (Local)
32 S-dead1 Storage Dead imports in image.service.ts
33 S-dead2 Storage Triple Cloudinary configuration
34 CA-L1 Cache validateCacheOwnership returns true for "no cache"
35 CA-L2 Cache Triple userId validation
36 CA-L3 Cache getCacheStats double-fetches getPermissionCache
37 CA-L4 Cache set() uses if (ttlSeconds) — falsy for 0
38 E-E5 Email verify() on every send in email.ts
39 CA-L10 Cache All ICache methods async for sync ops
40 E-TM-dup Email Two transporterMail with different options

Modification Guidance

To Add a New Email Provider

  1. Create src/lib/email/NewProvider.ts implementing IEmailService
  2. Add case "newprovider" to EmailFactory.getEmailService() switch
  3. Add EMAIL_SERVICE=newprovider to .env and .env.example
  4. WARNING: Must also update helper/email.ts if any consumers use the legacy path
  5. Ensure error handling matches expected behavior (throw on failure, do NOT swallow)

To Add a New Payment Provider

  1. Create src/lib/payment/NewPaymentService.ts implementing IPaymentService
  2. Add case "newprovider" to PaymentFactory.create() switch
  3. Fix paymentService.ts to pass paymentGateway to factory (currently hardcoded)
  4. Update PaymentProvider type in payment.type.ts

To Unify Storage Systems

  1. Option A (Migrate to factory): Wire handleFileUpload consumers to use StorageFactory → fix factory bugs → add "local" case → fix LocalSotrage filename → fix base64 write
  2. Option B (Delete factory): Remove entire lib/storage/ (250 LOC) → document helper layer as canonical → fix helper layer bugs

To Unify Cache Systems

  1. Option A (Migrate to lib/cache): Create CacheFactory → migrate shared/cache.ts consumers → fix TTL mismatches → add maxKeys → fix useClones
  2. Option B (Delete lib/cache): Remove lib/cache/ (37 LOC) → fix shared/cache.ts TTL comments → add maxKeys limit

Testing Checklist for Changes

  • Verify EMAIL_SERVICE env var is set before testing email factory
  • Test each email provider with mock SMTP/API
  • Verify Stripe checkout creates session with correct URLs
  • Test cancel URL redirect (currently broken)
  • Verify null-Stripe throws instead of returning undefined
  • Test storage upload with each provider type
  • Verify local storage writes binary not base64 text
  • Test cache TTL matches intended values (not comments)
  • Verify cache doesn't grow unbounded under load
  • Test webhook signature verification for both Stripe and LemonSqueezy
  • Verify BrevoService propagates errors to callers

Systemic Findings

  1. Incomplete Migration: The lib/ factories appear to be a refactoring that was ~11% completed. Two subsystems (storage, cache) were never wired up. Email was partially wired (2 of 6 consumers). Payment was minimally wired (1 consumer that defeats the purpose by hardcoding).

  2. Zero Test Coverage: None of the 4 factory subsystems have any test files. This is the root cause of most bugs persisting (silent failures, wrong URLs, dead code, swapped arguments).

  3. Secret Management Gap: Only Stripe supports encrypted DB-stored secrets (via AppConfig). All other API keys are plaintext env vars. Real SMTP credentials are committed to .env.

  4. No Factory for Cache: Unlike email, payment, and storage, cache has no factory pattern at all — just a dead OOP wrapper and a production functional singleton.

  5. Console.log in Production: Multiple factory methods log to stdout on every call (EmailFactory, NodeMailerService, BrevoService).

  6. Architectural Drift: Each factory follows slightly different patterns (class vs function factory, singleton vs new-per-call, types in separate file vs bundled), making it hard to reason about the system holistically.


Subagent Reports

Detailed per-subsystem findings are available at: - _bmad-output/extra-docs/email-factory-deep-dive/ — 39 issues (7C, 8H, 10M, 14L) - _bmad-output/extra-docs/payment-factory-deep-dive/ — 42 issues (7C, 10H, 14M, 11L) - _bmad-output/extra-docs/storage-factory-deep-dive/ — 42 issues (8C, 9H, 14M, 11L) - _bmad-output/extra-docs/cache-deep-dive/ — 22 issues (5C, 6H, 7M, 4L) - _bmad-output/extra-docs/factory-cross-cutting-analysis/ — Cross-factory consumer mapping, competing systems inventory, AppConfig analysis


Generated by document-project workflow (deep-dive mode) | Exhaustive scan with 5 parallel subagents Base Documentation: docs/index.md Scan Date: 2026-03-29 Analysis Mode: Exhaustive