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_emailbinding 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-sesThis 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:
| Endpoint | When Sent |
|---|---|
/ses/send-verification | Signup email verification |
/ses/send-password-reset | Password reset request |
/ses/send-magic-link | Passwordless sign-in link |
/ses/send-otp | One-time password code |
/ses/send-welcome | Post-signup welcome email |
/ses/send-organization-invitation | Team/org invite with role |
| Delete account | Account 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:
| Key | When 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 ignoresplugins.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
| Variable | Description |
|---|---|
AWS_ACCESS_KEY_ID | AWS access key (use Wrangler secrets) |
AWS_SECRET_ACCESS_KEY | AWS secret key (use Wrangler secrets) |
AWS_SES_REGION | SES region (e.g., us-east-2) |
EMAIL_FROM | Sender email address (must be SES-verified) |
Optional Variables
| Variable | Description |
|---|---|
EMAIL_FROM_NAME | Sender display name |
EMAIL_REPLY_TO | Reply-to address (defaults to EMAIL_FROM) |
APP_NAME | Application name shown in email templates |
APP_URL | Application URL for links in emails |
BETTER_AUTH_URL | Auth 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
- Auth Configuration — Configuring auth plugins