platform-codebase/@packages/@infrastructure/email-client
2026-02-23 16:52:53 -08:00
..
src
eslint.config.js
package.json
README.md
tsconfig.json
tsup.config.ts
vitest.config.ts

@lilith/email-client

Shared NestJS client for sending emails through the centralized email service. Any backend feature that needs to send email should use this package rather than implementing direct SMTP/nodemailer.

Installation

Add to your feature's package.json:

{
  "dependencies": {
    "@lilith/email-client": "*"
  }
}

Usage

1. Import the module

// app.module.ts
import { EmailClientModule } from '@lilith/email-client'

@Module({
  imports: [
    EmailClientModule.forRoot(),
    // ...
  ],
})
export class AppModule {}

The module is @Global() — once imported in AppModule, EmailClientService is injectable everywhere.

2. Inject the service

import { EmailClientService } from '@lilith/email-client'

@Injectable()
export class MyService {
  constructor(private readonly emailClient: EmailClientService) {}

  async notifyUser() {
    await this.emailClient.sendTemplate({
      to: 'user@example.com',
      templateName: 'my-feature/notification',
      variables: { name: 'Alice', action: 'completed' },
      category: 'my-feature',
      priority: 'normal',
    })
  }
}

API

Typed methods (SSO/auth emails)

Method Purpose
sendWelcome(data) Welcome email to new user
sendVerification(data) Email verification link
sendPasswordReset(data) Password reset (accepts resetToken, NOT resetUrl)
sendPasswordChanged(data) Password changed confirmation
sendAccountLocked(data) Account locked notification
sendLoginAlert(data) New login alert
sendOtp(data) MFA OTP code

Generic methods (any feature)

Method Purpose
sendTemplate(options) Send via named Handlebars template
sendCustom(options) Send with inline HTML content

SendTemplateEmailOptions

interface SendTemplateEmailOptions {
  to: string | string[]
  templateName: string              // e.g. 'qa/report-alert', 'merch/approval'
  variables: Record<string, unknown>
  category?: string
  userId?: string
  priority?: 'high' | 'normal' | 'low'
}

SendCustomEmailOptions

interface SendCustomEmailOptions {
  to: string | string[]
  subject: string
  html: string
  text?: string
  category?: string
  userId?: string
  priority?: 'high' | 'normal' | 'low'
}

Configuration

Defaults (zero-config)

With EmailClientModule.forRoot() and no arguments, the client:

  1. Resolves the email service URL from @lilith/service-registry
  2. Reads the API key from EMAIL_INTERNAL_API_KEY env var
  3. If the API key is missing, email sending is disabled (logs a warning, never throws)

Custom options

EmailClientModule.forRoot({
  serviceUrl: 'http://custom-email:3011',  // Override URL
  apiKeyEnvVar: 'MY_API_KEY_VAR',          // Override env var name
})

Async options

EmailClientModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    serviceUrl: config.get('EMAIL_URL'),
  }),
})

Environment Variables

Variable Required Default Purpose
EMAIL_INTERNAL_API_KEY Yes API key for email service internal endpoints
EMAIL_SERVICE_URL No service-registry Override email service URL

Error Handling

All methods are graceful — email failures log errors but never throw. This ensures email delivery issues never break calling service flows. Methods return string | null (the job ID, or null on failure).

Current Consumers

Feature Methods Used
SSO All 7 typed methods (welcome, verification, reset, etc.)
Platform Admin sendTemplate() for QA report alerts
Landing sendTemplate() for merch approval/rejection
QA Backend Domain events (indirect via email service QA processor)

Internal Architecture

Your Service
    │
    │  emailClient.sendTemplate({...})
    ▼
EmailClientService (this package)
    │
    │  POST /internal/send/template
    │  Header: X-Internal-Api-Key
    ▼
Email Service (codebase/features/email/backend-api)
    │
    │  BullMQ queue → Handlebars render → Nodemailer
    ▼
SMTP → Recipient