Quickback Docs
Plugins

AWS SES Plugin

@quickback-dev/better-auth-aws-ses — AWS SES email delivery for Better Auth.

@quickback-dev/better-auth-aws-ses is a Better Auth plugin that sends transactional emails via AWS SES. It handles AWS Signature V4 signing, HTML/text email templates, and rate limiting — designed to work in Cloudflare Workers (no Node.js aws-sdk dependency).

Cloudflare Email is the default. As of the Cloudflare Email Sending public beta, the zero-config send_email binding is Quickback's default transport for Cloudflare deploys — no API keys, no SES setup. See Email Configuration. Use AWS SES when you need it (non-Cloudflare runtimes, existing SES infra, or specific deliverability/compliance requirements).

Installation

npm install @quickback-dev/better-auth-aws-ses

This plugin is wired up only when you explicitly opt in via email.provider: 'aws-ses' in your Quickback config. Without that, email.provider defaults to 'cloudflare' and the Cloudflare Email helper is used instead.

Email Types

The plugin handles 8 email types:

EndpointWhen Sent
/ses/send-verificationSignup email verification
/ses/send-password-resetPassword reset request
/ses/send-magic-linkPasswordless sign-in link
/ses/send-otpOne-time password code
/ses/send-welcomePost-signup welcome email
/ses/send-organization-invitationTeam/org invite with role
Delete accountAccount deletion confirmation

AWS Signature V4

The plugin signs requests to SES using AWS Signature V4 with the Web Crypto API (crypto.subtle), making it compatible with Cloudflare Workers without any AWS SDK dependency.

Templates

Each email type has both HTML and plain text templates. Templates include:

  • Responsive HTML layout with MSO (Microsoft Outlook) support
  • Security warning banners for sensitive operations
  • Fallback plain text versions for all email clients
  • XSS prevention via HTML escaping and URL sanitization

Per-Type Template Overrides

Override the default subject + HTML + text for any OTP type without forking the package. Drop a .ts file into quickback/email-templates/ and reference it from plugins.emailOtp.templates:

// quickback/quickback.config.ts
defineAuth("better-auth", {
  plugins: {
    emailOtp: {
      templates: {
        'forget-password': './email-templates/forget-password',
        'welcome':         './email-templates/welcome',
      },
    },
  },
});

Each template module default-exports a function that returns { subject, html, text }:

// quickback/email-templates/welcome.ts
interface Input {
  email: string;
  otp: string;
  type: 'sign-in' | 'email-verification' | 'forget-password' | 'welcome';
  name?: string;
  appName: string;
  appUrl: string;
  supportEmail?: string;
}

export default function welcomeTemplate({ otp, name, appName, appUrl }: Input) {
  const greeting = name ? `Hi ${name},` : 'Hi,';
  return {
    subject: `Welcome to ${appName} — set your password`,
    html: `
      <h1>Welcome to ${appName}</h1>
      <p>${greeting} your account is ready. Use this code to set your password and sign in:</p>
      <p style="font-size:32px;letter-spacing:8px"><b>${otp}</b></p>
      <p>Visit <a href="${appUrl}/set-password">${appUrl}/set-password</a> to finish setup.</p>
    `,
    text: `${greeting}\n\nYour ${appName} code: ${otp}\n\nFinish setup at ${appUrl}/set-password`,
  };
}

Recognized keys:

KeyWhen it fires
'sign-in'OTP sign-in flow
'email-verification'Signup email verification
'forget-password'Password reset request — and admin-bootstrap if welcome isn't set
'welcome'Synthetic — routed when forget-password fires for a user with no credential row yet (the admin-plugin → bootstrap flow). Without it, those sends fall through to forget-password.

Missing keys fall through to the built-in defaults from @quickback-dev/better-auth-aws-ses. The compiler stages each referenced file into src/email-templates/<name>.ts and wires the default import in src/lib/auth.ts — the template function is called inline from sendVerificationOTP, so it has access to whatever modules the file imports.

Provider scope. Overrides are only honored when email.provider: 'aws-ses'. The Cloudflare Email path uses the helper's own template pipeline and ignores plugins.emailOtp.templates.

Configuration

Server Plugin

import { awsSESPlugin } from "@quickback-dev/better-auth-aws-ses";

const auth = betterAuth({
  plugins: [
    awsSESPlugin({
      region: env.AWS_SES_REGION,
      accessKeyId: env.AWS_ACCESS_KEY_ID,
      secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
      fromEmail: env.EMAIL_FROM,
      fromName: env.EMAIL_FROM_NAME || "My App",
      replyTo: env.EMAIL_REPLY_TO,
      appName: env.APP_NAME || "My App",
      appUrl: env.APP_URL,
    }),
  ],
});

Client Plugin

import { awsSESClientPlugin } from "@quickback-dev/better-auth-aws-ses/client";

const authClient = createAuthClient({
  plugins: [awsSESClientPlugin()],
});

Required Environment Variables

VariableDescription
AWS_ACCESS_KEY_IDAWS access key (use Wrangler secrets)
AWS_SECRET_ACCESS_KEYAWS secret key (use Wrangler secrets)
AWS_SES_REGIONSES region (e.g., us-east-2)
EMAIL_FROMSender email address (must be SES-verified)

Optional Variables

VariableDescription
EMAIL_FROM_NAMESender display name
EMAIL_REPLY_TOReply-to address (defaults to EMAIL_FROM)
APP_NAMEApplication name shown in email templates
APP_URLApplication URL for links in emails
BETTER_AUTH_URLAuth API URL (for verification/reset links)

Rate Limiting

The plugin includes basic in-memory rate limiting per recipient:

  • Configurable hourly and daily limits
  • Per-recipient tracking
  • For production, consider using an external store (Redis, KV)
awsSESPlugin({
  // ...
  rateLimit: {
    maxEmailsPerHour: 10,
    maxEmailsPerDay: 50,
  },
})

Error Handling

  • Email failures return false (don't throw) — signup/login flows continue even if email delivery fails
  • Invalid or placeholder AWS credentials are detected at startup
  • Detailed console logging for debugging delivery issues

See Also

On this page