Search docs

Jump between documentation pages.

Send email from DaloyJS with AWS SES

Amazon Simple Email Service (SES) is AWS's pay-as-you-go transactional and bulk email service. This guide uses SESv2 — the current API — through the AWS SDK for JavaScript v3. Best fit when you already run on AWS (Lambda, ECS, Fargate, EC2) or need very low per-message cost.

1. Provision

  1. In the AWS console, open Amazon SES → Verified identities and verify either an email address (for development) or your sending domain (for production). Add the SPF, DKIM, and DMARC records SES shows you.
  2. New accounts start in the SES sandbox: you can only send to verified addresses. Request production access from Account dashboard before launching.
  3. Create an IAM principal that can call ses:SendEmail. Prefer an execution role attached to your Lambda or container — avoid long-lived access keys.

2. Install

ts
pnpm add @aws-sdk/client-sesv2

The v3 SDK is modular — install only the SESv2 client. It works on Node 18+ and is compatible with the Lambda adapter.

3. Environment variables

ts
# .env
AWS_REGION=us-east-1
SES_FROM_ADDRESS="Acme <no-reply@acme.example.com>"

# Local development only — in AWS, prefer the execution role
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...

4. Plugin

The SDK reads credentials from the standard AWS provider chain (env vars, shared config file, IMDS, IRSA on EKS, Lambda role), so the client itself takes no secrets.

ts
// src/plugins/ses.ts
import {
  SESv2Client,
  SendEmailCommand,
  type SendEmailCommandInput,
} from "@aws-sdk/client-sesv2";
import type { App } from "@daloyjs/core";

const client = new SESv2Client({ region: process.env.AWS_REGION });

export const sesPlugin = {
  name: "ses",
  register(app: App) {
    app.decorate("email", {
      async send({ to, subject, text, html }) {
        const input: SendEmailCommandInput = {
          FromEmailAddress: process.env.SES_FROM_ADDRESS!,
          Destination: { ToAddresses: [to] },
          Content: {
            Simple: {
              Subject: { Data: subject, Charset: "UTF-8" },
              Body: {
                ...(text ? { Text: { Data: text, Charset: "UTF-8" } } : {}),
                ...(html ? { Html: { Data: html, Charset: "UTF-8" } } : {}),
              },
            },
          },
        };
        const out = await client.send(new SendEmailCommand(input));
        return { id: out.MessageId ?? "" };
      },
    });
    app.onClose(() => client.destroy());
  },
};

declare module "@daloyjs/core" {
  interface AppState {
    email: {
      send(msg: {
        to: string;
        subject: string;
        text?: string;
        html?: string;
      }): Promise<{ id: string }>;
    };
  }
}

5. Use it in a route

ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { sesPlugin } from "./plugins/ses";

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 10 }));
app.register(sesPlugin);

app.route({
  method: "POST",
  path: "/notify",
  operationId: "sendWelcome",
  request: {
    body: z.object({
      to: z.string().email(),
      name: z.string().min(1).max(80),
    }),
  },
  responses: {
    202: { description: "Queued", body: z.object({ id: z.string() }) },
  },
  handler: async ({ body, state }) => {
    const { id } = await state.email.send({
      to: body.to,
      subject: `Welcome, ${body.name}!`,
      text: `Hi ${body.name}, thanks for signing up.`,
      html: `<p>Hi ${body.name}, thanks for signing up.</p>`,
    });
    return { status: 202, body: { id } };
  },
});

Templates & attachments

SESv2's SendEmailCommand accepts three content variants:

  • Content.Simple — subject + text/HTML body (used above). Also supports an Attachments array with base64 RawContent, FileName, and ContentType.
  • Content.Raw.Data — a fully MIME-encoded message (use mailcomposer or nodemailer's composer if you need rich attachments).
  • Content.Template — render an SES template by TemplateName with a JSON TemplateData payload. Create templates ahead of time with CreateEmailTemplateCommand.

Runtimes

  • Node / Bun / Deno / AWS Lambda — works out of the box. On Lambda, omit access keys and let the execution role supply credentials.
  • Cloudflare Workers / Vercel Edge — the SDK can run there but uses a Web Crypto signer; pin @aws-sdk/client-sesv2 ≥ 3.700 and pass credentials explicitly (the default provider chain expects Node APIs).

Observability

SES publishes delivery, bounce, and complaint events to SNS, EventBridge, or Kinesis Firehose via a configuration set. Add ConfigurationSetName to SendEmailCommandInput to opt in.

See also Resend, SendGrid, and the email integrations overview.