Email

Maizzle templates, inline vs Cloudflare Queue delivery, and the console sender in development.

Transactional mail is rendered by @taman/emails and sent from apps/api/server/email.

Templates

Maizzle project under packages/server/emails. Scripts:

pnpm --filter @taman/emails build:templates
pnpm --filter @taman/emails dev:templates

Templates and params:

TemplateTrigger
invitationorganization sendInvitationEmail
verify-emailemailVerification.sendVerificationEmail
reset-passwordemailAndPassword.sendResetPassword

Locales: en-US, id-ID. renderEmail(job) returns subject + HTML (+ plaintext from the Maizzle build). Invalid jobs throw EmailJobError.

NITRO_APP_URL is used for invitation links. From: NITRO_EMAIL_FROM / NITRO_EMAIL_FROM_NAME.

Sender and dispatcher

createEmailServices(env, config) in server/email/index.ts:

BindingBehavior
env.EMAIL presentcreateCloudflareEmailSender (send_email binding named EMAIL)
elsecreateConsoleEmailSender — logs the message (local nitro dev)
env.EMAIL_QUEUE presentenqueue via EMAIL_QUEUE producer
elsecreateInlineEmailDispatcher — deliver immediately

The Cloudflare queue is declared in nitro.config.ts:

  • Producer binding EMAIL_QUEUE → queue taman-email (renamed to <name>-email on generate)
  • Consumer: max_retries: 5, DLQ taman-email-dlq
  • Plugin server/plugins/email-queue.ts handles cloudflare:queue when batch.queue === 'taman-email'

Poison messages: EmailJobError is acked (will not retry). Other errors retry until the DLQ.

Wiring from auth

better-auth.instance.ts dispatches jobs; it does not send SMTP itself. Locale comes from emailLocale(request).

Local development

No Cloudflare bindings → console sender + inline dispatcher. Watch the API terminal for HTML dumps. You still need valid NITRO_EMAIL_FROM for the deliverer config.

Production checklist

  • Create queues <name>-email and <name>-email-dlq in the Cloudflare account (names must match wrangler output after generate)
  • Enable Email Routing / send_email for the Worker
  • Set NITRO_EMAIL_FROM to a domain you control
  • Confirm the consumer Worker is the same deployment as the API (deployConfig: true on Nitro)

Exact Cloudflare dashboard clicks are not verified in-repo; derive names from nitro.config.ts and template.manifest.ts.