Transactional email via AWS SES, SendGrid, and Brevo.
We support three email providers. All services use a shared EmailService interface — swap providers via environment variable without changing call sites.
Provider matrix
| Provider | Use when | Guide |
|---|---|---|
| AWS SES | Default for production — lowest cost, already on AWS | AWS SES |
| SendGrid | Rich template editor, marketing + transactional | SendGrid |
| Brevo | SMTP relay, EU data residency, contact management | Brevo |
Shared interface
// src/services/email/email.types.ts
export interface EmailMessage {
to: string | string[];
subject: string;
html: string;
text?: string;
replyTo?: string;
tags?: string[];
}
export interface EmailProvider {
send(message: EmailMessage): Promise<{ messageId: string }>;
}Factory
// src/services/email/index.ts
import { SesProvider } from "./ses.provider";
import { SendGridProvider } from "./sendgrid.provider";
import { BrevoProvider } from "./brevo.provider";
const providers = {
ses: SesProvider,
sendgrid: SendGridProvider,
brevo: BrevoProvider,
};
export function getEmailProvider() {
const key = process.env.EMAIL_PROVIDER ?? "ses";
const Provider = providers[key as keyof typeof providers];
if (!Provider) throw new Error(`Unknown EMAIL_PROVIDER: ${key}`);
return new Provider();
}
export async function sendEmail(message: EmailMessage) {
return getEmailProvider().send(message);
}AWS SES provider
pnpm add @aws-sdk/client-ses// src/services/email/ses.provider.ts
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import type { EmailProvider, EmailMessage } from "./email.types";
export class SesProvider implements EmailProvider {
private client = new SESClient({ region: process.env.AWS_REGION });
async send(message: EmailMessage) {
const result = await this.client.send(
new SendEmailCommand({
Source: process.env.EMAIL_FROM!,
Destination: { ToAddresses: [].concat(message.to) },
Message: {
Subject: { Data: message.subject },
Body: {
Html: { Data: message.html },
Text: { Data: message.text ?? "" },
},
},
ReplyToAddresses: message.replyTo ? [message.replyTo] : undefined,
}),
);
return { messageId: result.MessageId! };
}
}SendGrid provider
pnpm add @sendgrid/mail// src/services/email/sendgrid.provider.ts
import sgMail from "@sendgrid/mail";
import type { EmailProvider, EmailMessage } from "./email.types";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
export class SendGridProvider implements EmailProvider {
async send(message: EmailMessage) {
const [response] = await sgMail.send({
to: message.to,
from: process.env.EMAIL_FROM!,
subject: message.subject,
html: message.html,
text: message.text,
replyTo: message.replyTo,
categories: message.tags,
});
return { messageId: response.headers["x-message-id"] as string };
}
}Brevo provider
pnpm add @getbrevo/brevo// src/services/email/brevo.provider.ts
import * as Brevo from "@getbrevo/brevo";
import type { EmailProvider, EmailMessage } from "./email.types";
export class BrevoProvider implements EmailProvider {
private api = new Brevo.TransactionalEmailsApi();
private apiKey = Brevo.TransactionalEmailsApiApiKeys.apiKey;
constructor() {
this.api.setApiKey(this.apiKey, process.env.BREVO_API_KEY!);
}
async send(message: EmailMessage) {
const result = await this.api.sendTransacEmail({
sender: { email: process.env.EMAIL_FROM! },
to: [].concat(message.to).map((email) => ({ email })),
subject: message.subject,
htmlContent: message.html,
textContent: message.text,
replyTo: message.replyTo ? { email: message.replyTo } : undefined,
tags: message.tags,
});
return { messageId: result.body.messageId! };
}
}Usage in services
// src/services/auth.service.ts
import { sendEmail } from "./email";
export async function sendPasswordReset(email: string, token: string) {
const link = `${process.env.APP_URL}/reset?token=${token}`;
await sendEmail({
to: email,
subject: "Reset your password",
html: `<p>Click <a href="${link}">here</a> to reset your password. Link expires in 1 hour.</p>`,
tags: ["password-reset"],
});
}Environment
EMAIL_PROVIDER=ses # ses | sendgrid | brevo
EMAIL_FROM=noreply@example.com
# SES — uses AWS task role in ECS
AWS_REGION=us-east-1
# SendGrid
SENDGRID_API_KEY=SG.xxx
# Brevo
BREVO_API_KEY=xkeysib-xxx