Email Delivery & Worker System
Authara includes a built-in email system for verification, account-security, and organization notifications. Core queues messages for:
- signup, password-reset, and email-change codes;
- account creation, new sign-ins, account disablement, and re-enablement;
- authentication-method additions and removals, password changes, and completed email changes (to both the old and new addresses);
- admin-access changes;
- organization invitations, accepted or revoked invitations, membership and role changes, ownership transfers, and organization deletion.
Organization invitation emails always contain both the hosted invitation link
and the raw invitation code. Deployments upgrading from versions that exposed
AUTHARA_INVITATION_EMAIL_INCLUDE_CODE should remove that setting; invitation
codes are no longer conditionally omitted.
The system is designed to be:
- reliable
- asynchronous
- durable across worker restarts
- at-least-once, with possible duplicate delivery
Overview
Email delivery in Authara is handled via a job-based worker system.
Instead of sending emails directly during a request:
- an email job is created
- the job is stored in the database
- background workers process the job
- the email is sent via the configured provider
Operators can customize each transactional email from the operator workspace. The worker resolves the currently saved template when it processes a job, so changes apply without restarting Core. A job that was queued before a template change uses the latest saved template when it is delivered. Restoring a template immediately returns subsequent deliveries to the built-in version. See Operator email templates for provisioning, history, audit, backup, and recovery guidance.
Operators can also disable or re-enable each email type from the template
catalog. Every type is enabled by default. A disabled type does not create new
email_jobs; jobs already queued before it was disabled are unaffected. There
are no per-notification environment toggles. When enabled, a new-sign-in
notification is created for every new authenticated session and includes the
observed client IP address and user agent when available.
Account deletion intentionally does not queue an email. The deletion flow removes queued jobs and other direct references to the user's email address, so queuing a final message would conflict with that privacy boundary.
How it works
Step 1 — Job creation
When an email needs to be sent:
- a record is inserted into
email_jobs - status is set to
pending
Step 2 — Worker processing
Workers continuously:
- poll for pending jobs
- attempt delivery
- update job status
Step 3 — Delivery result
Depending on the outcome:
sent→ success- transient failure → retry scheduled with capped exponential backoff and jitter
- permanent failure → job stops retrying immediately
- expired delivery deadline or exhausted attempt limit → job stops retrying
Workers claim jobs with a processing lease and increment attempt_count before
delivery. Every sent, retry, or failed transition must still own that lease. If
a worker crashes, a reaper safely returns the expired lease to pending (or
marks it failed when its delivery bounds have been reached). During shutdown,
workers stop claiming new jobs and are given a bounded drain period for
in-flight delivery.
Providers
noop (default)
AUTHARA_EMAIL_PROVIDER=noop
- no emails are sent
- email metadata is logged
- useful for development
The noop provider is rejected in production because password recovery routes are always available and require deliverable email. Development starts the worker with this provider but emits an explicit warning that recovery and security messages will not reach recipients.
smtp
AUTHARA_EMAIL_PROVIDER=smtp
Uses an SMTP server (e.g. Mailgun, Mailjet, SES).
SMTP is required in production, independently of
AUTHARA_CHALLENGE_ENABLED.
SMTP configuration
AUTHARA_EMAIL_FROM
Sender email address.
Example:
no-reply@mg.example.com
Required when using SMTP.
AUTHARA_EMAIL_SMTP_HOST
SMTP server host.
Example:
smtp.mailgun.org
AUTHARA_EMAIL_SMTP_PORT
Default:
587
AUTHARA_EMAIL_SMTP_USERNAME
SMTP username.
AUTHARA_EMAIL_SMTP_PASSWORD
SMTP password.
AUTHARA_EMAIL_SMTP_TLS
Enable TLS.
Default:
true
AUTHARA_EMAIL_SMTP_TIMEOUT
Timeout for SMTP operations.
Default:
10s
Worker configuration
AUTHARA_EMAIL_WORKER_COUNT
Number of concurrent workers.
Default:
2
Higher values:
- increase throughput
- increase load on SMTP provider
AUTHARA_EMAIL_WORKER_POLL_INTERVAL
How often workers check for new jobs.
Default:
2s
AUTHARA_EMAIL_JOB_MAX_ATTEMPTS
Maximum delivery attempts per job, including attempts interrupted by a worker crash.
Default:
100
If the environment variable is absent, an operator can change this value at runtime. The new limit is used when the next failed attempt is evaluated, including for jobs that are already queued.
Transient delivery uses equal-jitter exponential backoff. The first retry is scheduled 15–30 seconds later, subsequent windows double, and the delay is capped at 6 hours. Security and account-notification jobs remain deliverable for 72 hours, so a provider outage lasting a day does not discard them. The attempt-limit default is deliberately high enough for that delivery window.
Time-sensitive jobs use their own earlier deadline:
- signup, password-reset, and email-change mail stops at challenge expiry;
- organization invitation mail stops at invitation expiry.
When a deadline is reached, the job becomes failed with terminal reason
delivery_deadline_exceeded. Operators can inspect attempt count, next attempt,
delivery deadline, terminal reason, last error, and queue age in the admin
delivery queue and failures view.
AUTHARA_EMAIL_PROCESSING_STALE_AFTER
Age after which an unfinished delivery is treated as an abandoned processing
lease. The value must be greater than AUTHARA_EMAIL_SMTP_TIMEOUT.
Default: 2m.
AUTHARA_EMAIL_STALE_REAPER_INTERVAL
How often workers look for abandoned processing leases.
Default: 1m.
AUTHARA_EMAIL_MAINTENANCE_BATCH_SIZE
Maximum number of stale email jobs reclaimed in one database batch.
Default: 1000.
Cleanup
Authara automatically cleans up old email records.
Only the replica holding the shared cleanup lease performs retention cleanup.
AUTHARA_EMAIL_CLEANUP_INTERVAL controls the startup-only schedule and defaults
to 1h. Cleanup is bounded and unfinished rows are resumed by a later pass.
AUTHARA_EMAIL_CLEANUP_SENT_AFTER
Delete successfully sent emails after:
720h (30 days)
If the environment variable is absent, an operator can change this retention at runtime. The next cleanup run uses the new cutoff.
AUTHARA_EMAIL_CLEANUP_FAILED_AFTER
Delete failed emails after:
2160h (90 days)
If the environment variable is absent, an operator can change this retention at runtime. The next cleanup run uses the new cutoff.
Failure handling
Temporary failure
Examples:
- SMTP timeout
- provider unavailable
Behavior:
- job is retried
- next attempt uses capped exponential backoff with jitter
- retries continue until the delivery deadline or maximum attempt count
Permanent failure
Examples:
- invalid domain
- rejected recipient
Behavior:
- job is marked as failed
- no further retries
SMTP 4xx replies and network failures are treated as transient. SMTP 5xx
replies, rejected recipients, invalid addresses, unrecoverable authentication,
and invalid job/configuration data are treated as permanent.
Delivery guarantees and duplicates
Email delivery is at-least-once, not exactly-once. If an SMTP server accepts a
message and Core crashes before recording sent, the expired lease is retried
and the recipient can receive a duplicate. Lease fencing prevents an old worker
from overwriting the state owned by a newer worker, but it cannot atomically
combine an external SMTP transaction with the database update.
Challenge-code messages generate the code during each delivery attempt. In the rare accepted-message/crash window, a retried duplicate can contain a newer code, and only the most recently generated code is valid. Requesting an explicit resend likewise makes the newly generated code authoritative.
Common issues
Emails not arriving
Check:
- DNS configuration (SPF, DKIM, MX)
- sender domain validity
- SMTP credentials
Emails marked as spam
Improve:
- SPF/DKIM alignment
- DMARC policy
- domain reputation
Provider rejection
Example error:
553 domain does not exist
Cause:
- missing DNS records (A/MX)
Development setup
For local development:
Mailpit
Run:
docker run -p 1025:1025 -p 8025:8025 axllent/mailpit:v1.31.1
Config:
AUTHARA_EMAIL_PROVIDER=smtp
AUTHARA_EMAIL_SMTP_HOST=localhost
AUTHARA_EMAIL_SMTP_PORT=1025
AUTHARA_EMAIL_FROM=test@test.com
Then open:
http://localhost:8025