Email Service Architecture & Developer Guide
Production-ready, modular email service for Oktivo. Provider-agnostic (swap Resend/SES/ZeptoMail with one env var change), queue-backed, fully logged.
Architecture Overview
File Structure
src/
├── config/
│ └── index.ts # EMAIL_PROVIDER, EMAIL_FROM, RESEND_API_KEY
├── models/
│ ├── EmailLog.ts # Audit log (90-day TTL)
│ ├── EmailToken.ts # Onboarding/verification tokens (30-min expiry)
│ └── User.ts # Added: emailVerified, isPasswordSet fields
├── services/
│ └── email/
│ ├── index.ts # Re-exports everything
│ ├── email.service.ts # Core: BullMQ queue, send(), sendImmediate()
│ ├── email.actions.ts # Business logic: all email trigger functions
│ ├── providers/
│ │ ├── email.provider.interface.ts # IEmailProvider contract
│ │ ├── resend.provider.ts # Resend REST API implementation
│ │ ├── ses.provider.ts # AWS SES implementation
│ │ └── index.ts # Re-exports
│ └── templates/
│ ├── base.template.ts # HTML layout, button, info table helpers
│ └── index.ts # All 15+ email templates
├── controllers/
│ └── emailAuth.controller.ts # Token verify, password setup, forgot password
└── routes/
├── auth.routes.ts # Added: verify-token, setup-password, etc.
├── admin.routes.ts # Added: resend-onboarding
└── superadmin.routes.ts # Added: resend-onboardingDatabase Changes
New Collections
| Collection | Purpose | TTL |
|---|---|---|
EmailLog | Audit trail for every email sent | 90 days |
EmailToken | Secure tokens for onboarding/verification/password reset | 24h after expiry |
Modified Collections
| Collection | New Fields | Purpose |
|---|---|---|
User | emailVerified: Boolean | Whether user verified their email |
User | isPasswordSet: Boolean | Whether user set their own password (vs temp) |
Email Flow Diagrams
Onboarding Flow (Org Admin or Customer)
Direct Signup Flow (Website)
Low Balance Alert Flow (Non-Spamming)
Resend Onboarding Link
All Email Triggers
SuperAdmin Emails
| Trigger | Template | Recipient | When |
|---|---|---|---|
| Org onboarded | onboarding_org | Org admin email | POST /superadmin/organizations |
| Org suspended | org_suspended | Org admin email | PUT /superadmin/organizations/:id status=suspended |
| Org unsuspended | org_unsuspended | Org admin email | PUT /superadmin/organizations/:id status=active |
| Resend onboarding | resend_onboarding_admin | Org admin email | POST /superadmin/resend-onboarding/:userUID |
| SIP trunk created | sip_trunk_created | Superadmin email | POST /superadmin/sip-trunks |
| SIP trunk updated | sip_trunk_updated | Superadmin email | PUT /superadmin/sip-trunks/:id |
| SIP trunk deleted | sip_trunk_deleted | Superadmin email | DELETE /superadmin/sip-trunks/:id |
| Phone number deleted | phone_number_deleted | Superadmin email | DELETE /superadmin/phone-numbers/:id |
| Provider capacity added | provider_capacity_created | Superadmin email | POST /superadmin/provider-capacity |
| Provider capacity updated | provider_capacity_updated | Superadmin email | PUT /superadmin/provider-capacity/:id |
| Provider capacity deleted | provider_capacity_deleted | Superadmin email | DELETE /superadmin/provider-capacity/:id |
| Org email verified | email_verification | Org admin email | POST /auth/admin/signup (direct signup) |
Organization (Admin) Emails
| Trigger | Template | Recipient | When |
|---|---|---|---|
| Customer onboarded | onboarding_customer | Customer email | POST /admin/customers |
| Customer suspended | customer_suspended | Customer email | PUT /admin/customers/:id status=suspended |
| Customer unsuspended | customer_unsuspended | Customer email | PUT /admin/customers/:id status=active |
| Resend onboarding | resend_onboarding_customer | Customer email | POST /admin/resend-onboarding/:userUID |
| Org pool balance < 500 | low_balance_organization_500 | Org admin email | After credit consumption |
Customer Emails
| Trigger | Template | Recipient | When |
|---|---|---|---|
| Agent created | agent_created | Customer email | POST /create-agent |
| Agent deleted | agent_deleted | Customer email | DELETE /delete-agent/:id |
| Campaign created | campaign_created | Customer email | POST /upload-campaign |
| Campaign started | campaign_started | Customer email | POST /campaigns/:id/start |
| Campaign stopped | campaign_stopped | Customer email | POST /campaigns/:id/stop |
| Campaign deleted | campaign_deleted | Customer email | DELETE /campaigns/:id |
| Campaign auto-stopped | campaign_auto_stopped | Customer email | Scheduler (9PM IST TRAI window) |
| Campaign auto-resumed | campaign_auto_started | Customer email | Scheduler (9AM IST TRAI window) |
| Campaign report | campaign_report | Customer email | POST /export-campaign/:id/email (CSV attachment) |
| Number assigned | number_assigned | Customer email | POST /admin/phone-numbers/:id/assign-customer |
| Credit balance < 500 | low_balance_customer_500 | Customer email | After credit consumption |
| Credit balance < 100 | low_balance_customer_100 | Customer email | After credit consumption (final warning) |
Public Auth Emails
| Trigger | Template | Recipient | When |
|---|---|---|---|
| Email verification | email_verification | User email | POST /auth/admin/signup |
| Password reset | password_reset | User email | POST /auth/forgot-password |
Switching Email Providers
Change one env var, restart. Zero code changes.
bash
# Current: Resend
EMAIL_PROVIDER=resend
RESEND_API_KEY=re_xxxxx
# Switch to SES (when you get prod access)
EMAIL_PROVIDER=ses
# Uses existing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from .env
# SES_REGION=ap-south-1 (optional, defaults to AWS_REGION)Adding a New Provider
- Create
src/services/email/providers/zepto.provider.ts - Implement
IEmailProviderinterface (justnameandsend()) - Add to
server.tsinitialization:
typescript
const emailProvider = config.email.provider === "zepto"
? new ZeptoProvider(config.email.zeptoApiKey, config.email.from)
: config.email.provider === "ses"
? new SESProvider(...)
: new ResendProvider(...);Queue & Retry Behavior
| Setting | Value |
|---|---|
| Queue name | email-queue |
| Max retries | 3 |
| Backoff | Exponential, starting at 5s |
| Concurrency | 5 workers |
| Rate limit | 10 emails/sec |
| Completed job retention | Last 1000 |
| Failed job retention | Last 5000 |
Critical emails (password reset, email verification) use sendImmediate() which bypasses the queue.
API Endpoints
Public (No Auth)
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/verify-token | Validate an email token |
| POST | /auth/setup-password | Set password via onboarding/reset token |
| POST | /auth/verify-email | Verify email via verification token |
| POST | /auth/forgot-password | Request password reset email |
| POST | /auth/resend-verification | Resend verification email |
SuperAdmin
| Method | Path | Purpose |
|---|---|---|
| POST | /superadmin/resend-onboarding/:userUID | Resend onboarding link to org admin |
Admin (Org)
| Method | Path | Purpose |
|---|---|---|
| POST | /admin/resend-onboarding/:userUID | Resend onboarding link to customer |
Customer
| Method | Path | Purpose |
|---|---|---|
| POST | /export-campaign/:campaignId/email | Email campaign report as CSV attachment |