Email Triggers
BluAuth owns every auth-domain email sent in the Blu ecosystem: invitations, password resets, welcome emails, and (roadmap) email verification. Downstream apps do not send these emails themselves — they trigger a BluAuth operation which causes BluAuth to send the email, themed for the tenant that initiated the action.
This page describes each trigger, the template that renders it, the variables substituted, how to override the template per-tenant via Theme Studio, how to test without spamming real inboxes, and the SES/DNS requirements for custom sender domains.
Delivery pipeline
- A BluAuth operation triggers an email (invitation created, password-reset requested, signup completed).
- BluAuth resolves the tenant theme for the triggering client (
resolveEmailTheme(clientId)). - The template renders themed HTML + plain-text bodies via
renderEmailBase+ per-concept template module. sendEmail()dispatches through AWS SES v2 (SendEmailCommand) with the configuredFromaddress and SES configuration set.
Source: server/utils/email/send.ts, server/utils/email/resolve-theme.ts, shared/utils/email/templates/.
The four emails
| When it sends | Template | Sender | |
|---|---|---|---|
| Invitation | Admin creates an invitation for a new user | shared/utils/email/templates/invitation.ts | BLUAUTH_SES_FROM_ADDRESS, display name = theme name |
| Password Reset | User submits /forgot-password | shared/utils/email/templates/password-reset.ts | BLUAUTH_SES_FROM_ADDRESS, display name = theme name |
| Welcome | User accepts invitation or completes self-signup | shared/utils/email/templates/welcome.ts | BLUAUTH_SES_FROM_ADDRESS, display name = theme name |
| Email verification (roadmap) | User changes their primary email | — | — |
All four emails route through the same sendEmail() helper, the same themed base chrome, and the same SES configuration set. They differ only in subject line, body HTML, and CTA link.
Template variables
Each template renders from a small, explicit set of inputs. No user-controlled HTML is interpolated.
Invitation — renderInvitationEmail
| Variable | Example | Notes |
|---|---|---|
theme | Resolved tenant theme object | Drives logo, primary color, font preset, footer text, tenant display name. |
acceptUrl | https://auth.example.com/invite?token=<opaque> | Single-use invitation link. Token valid until expiresAt. |
clientName | "Media Conductor" | Used in the headline copy ("You're invited to Media Conductor"). |
recipientName | "Jane Doe" or null | Greeting line. Falls back to "Hi there" when null. |
inviterName | "Clay Levering" | Personalizes the body ("Clay invited you"). |
expiresAt | Date | Rendered as a human-friendly expiry. |
Password reset — renderPasswordResetEmail
| Variable | Example | Notes |
|---|---|---|
theme | Resolved tenant theme object | Chrome and branding. |
resetUrl | https://auth.example.com/reset-password-token?token=<opaque> | Single-use link, 1-hour default TTL. |
userName | "Jane" or null | Greeting. |
Welcome — renderWelcomeEmail
| Variable | Example | Notes |
|---|---|---|
theme | Resolved tenant theme object | Chrome and branding. |
signInUrl | https://auth.example.com/login | CTA link. |
userName | "Jane" or null | Greeting. |
Theme resolution
At send time, BluAuth resolves the theme for the triggering client:
- Look up the OAuth client by ID.
- Load the
client_themesrow bound to the client. - Fall back to the service-default theme if no client-bound theme exists.
- Fall back to
HOSTED_LOGIN_THEME_DEFAULTSfor any missing fields.
Resolved values that affect email rendering:
name— tenant display name; appears as the SESFromdisplay name and in subject copy.primaryColor— hex color for CTA buttons and accent strokes.logoUrl— tenant logo rendered in the email header.footerText— legal/support line in the email footer.fontFamilyPreset— web-safe stack referenced in the email stylesheet.
Every email BluAuth sends is theme-accurate for the tenant initiating the action. Changes made in Theme Studio (/admin/themes) take effect on the next send without redeploy.
Triggering from downstream apps
You don't invoke the email sender directly. You trigger a BluAuth operation that causes BluAuth to send.
Trigger an invitation email
POST https://auth.example.com/api/admin/invitations
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"email": "invitee@example.com",
"recipientName": "First Last",
"clientId": "7f9b0e7e-...-a11c",
"expiresAt": "2026-04-23T00:00:00.000Z"
}
BluAuth creates the invitations row, publishes invitation.created, resolves the theme for clientId, and sends the invitation email via SES. The invitation email is not sent on invitation.revoked or invitation.accepted — only on creation.
Trigger a password-reset email
POST https://auth.example.com/api/auth/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}
This endpoint is exposed through the Better Auth catch-all at server/api/auth/[...all].ts. BluAuth creates a reset token, persists it, and sends the themed reset email. The handler always returns 200 — even when the address does not match a user — to prevent account enumeration.
Trigger a welcome email
You don't trigger it directly. BluAuth sends the welcome email automatically when:
- A user accepts an invitation at
/api/auth/accept-invite. - A user completes self-signup (where self-signup is enabled on the client).
If your app's sign-up flow creates a BluAuth user, the welcome email fires automatically. Don't send your own in parallel — users will receive duplicates.
What downstream apps should do
- Don't duplicate these emails. BluAuth owns the invitation, password-reset, and welcome templates. If your app wants to send a different welcome-to-the-product sequence after the auth welcome lands, subscribe to the
user.createdorinvitation.acceptedwebhook and drive your sequence from there. - Do subscribe to webhooks (webhook events) when you need to react to these lifecycle events — e.g. to create a tenant record in your app the moment a user signs up.
- Do customize your theme in
/admin/themes. Every email inherits the theme bound to the initiating client, including logo, colors, and footer copy.
Previewing and testing templates
Admins can preview and test-send any template from /admin/settings → Email test (backed by POST /api/admin/email-test).
Preview only (renders HTML, no SES send)
POST https://auth.example.com/api/admin/email-test
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"themeId": "b1f2c3d4-...-0001",
"recipientEmail": "qa@example.com",
"previewOnly": true,
"emailConcept": "invitation"
}
Response:
{
"success": true,
"data": {
"html": "<!doctype html>...",
"text": "You're invited...",
"theme": "Media Conductor",
"emailConcept": "invitation"
}
}
The admin UI renders this HTML in a sandboxed iframe so you can review layout, colors, logo, and copy without sending anything.
Test send (SES send to a real inbox)
Omit previewOnly (or set it to false):
{
"themeId": "b1f2c3d4-...-0001",
"recipientEmail": "qa-inbox@example.com",
"emailConcept": "password-reset"
}
The test send:
- Uses the real SES send path — DKIM/SPF alignment, bounce handling, and configuration-set routing are all exercised.
- Substitutes placeholder tokens (
test-preview-token-000) in CTA URLs. The links won't actually work — they're for visual QA only. - Subjects are prefixed
[TEST]so they're easy to filter in inboxes.
emailConcept accepts "password-reset", "welcome", or "invitation". The test route is admin-only.
Source: server/api/admin/email-test.post.ts.
Sender identity
- All emails send from
BLUAUTH_SES_FROM_ADDRESS(env var), e.g.no-reply@auth.example.com. - The SES
Fromheader uses the format<theme.name> <from@domain>so the user sees "Media Conductor" rather than "BluAuth" when the triggering tenant has a themed name. - Every message tags
source_app=bluauthvia SESEmailTagsfor cost attribution and log filtering. - The configuration set (env var
BLUAUTH_SES_CONFIGURATION_SET) routes bounce / complaint / delivery events to CloudWatch.
DKIM / SPF / DMARC for custom sender domains
If your tenant uses a custom sender domain (e.g. no-reply@mail.tenant.com instead of the shared BluAuth domain):
- Verify the domain in SES. The domain must be a verified identity in the AWS account BluAuth runs in.
- Publish the DKIM CNAMEs. SES generates three CNAMEs; all three must resolve.
- Align SPF. Add
include:amazonses.comto the domain's SPF record. SPF alignment on theFromdomain is required for DMARC pass. - Publish a DMARC record. Start with
p=noneand graduate top=quarantineonce you've verified alignment in SES + DMARC aggregate reports. - Do not send from unverified domains. SES rejects at send time.
Ask the BluAuth admin to update BLUAUTH_SES_FROM_ADDRESS (or introduce per-tenant senders once that's supported) after the DNS records propagate.
Delivery reliability
- SES bounces and complaints are recorded by SES and routed to the configuration set's event destinations (CloudWatch + SNS). They are not yet surfaced in the BluAuth admin UI — persistent bounces should be investigated in the SES console.
- BluAuth does not automatically pause invitations to a bouncing address. Admins should check SES's suppression list before re-inviting a bouncing user.
- Test sends are subject to the same bounce/complaint accounting as real sends. Use disposable test inboxes.
Localization
Not implemented — all emails are English. Localization is gated on the first tenant with a non-English primary locale. When added, locale will be resolved from the recipient's profile locale field, falling back to the tenant default, falling back to en-US.
Logging
Every send emits a structured log line with the template name, theme name, recipient email (hashed), SES message ID, and duration. Failures include the SES error code. Search the BluAuth logs with email.concept=<concept> to audit what's been sent to whom.