UXDL Docs

Email

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

ProviderUse whenGuide
AWS SESDefault for production — lowest cost, already on AWSAWS SES
SendGridRich template editor, marketing + transactionalSendGrid
BrevoSMTP relay, EU data residency, contact managementBrevo

Shared interface

typescript
// 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

typescript
// 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

bash
pnpm add @aws-sdk/client-ses
typescript
// 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

bash
pnpm add @sendgrid/mail
typescript
// 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

bash
pnpm add @getbrevo/brevo
typescript
// 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

typescript
// 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

dotenv
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

Official documentation