Setup wizard no longer requires email delivery. Admin account is auto-confirmed and auto-logged-in via token redirect. Adds setup secret gate for prod (logged on boot), SMTP env var config in runtime.exs, email_configured? helper, and admin warning banner when email isn't set up. Includes plan files for this task and the follow-up email settings UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.4 KiB
Email settings: admin UI with multi-adapter support
Status: Planned
Depends on: setup-auto-confirm.md (provides email_configured?/0 and warning banner)
Context
After the setup wizard auto-confirm work, SMTP is configurable via env vars only. Self-hosted users shouldn't need to restart the server or touch env vars to configure email. This plan adds an admin settings page for email delivery with support for popular Swoosh adapters, stored in the Settings table with encrypted secrets.
Design
Precedence chain
Env vars (SMTP_HOST etc.) > DB settings > Local adapter (dev default)
If SMTP_HOST is set, env vars win — the admin UI shows the config as read-only with a note that it's controlled by environment variables. If no env vars, the admin can configure everything from the UI. If nothing is configured, falls back to Swoosh.Adapters.Local.
Supported adapters
Start with the popular ones that cover 95%+ of use cases. Each has a simple config shape:
| Adapter | Module | Fields |
|---|---|---|
| SMTP | Swoosh.Adapters.SMTP |
relay (string), port (integer), username (string), password (secret) |
| Postmark | Swoosh.Adapters.Postmark |
api_key (secret) |
| Resend | Swoosh.Adapters.Resend |
api_key (secret) |
| SendGrid | Swoosh.Adapters.Sendgrid |
api_key (secret) |
| Mailgun | Swoosh.Adapters.Mailgun |
api_key (secret), domain (string) |
| Brevo | Swoosh.Adapters.Brevo |
api_key (secret) |
Adding more later is just adding an entry to the adapter registry — no code changes needed.
Adapter registry
A simple data structure defining each adapter's config shape:
defmodule Berrypod.Mailer.Adapters do
def all do
[
%{
key: "smtp",
name: "SMTP",
module: Swoosh.Adapters.SMTP,
fields: [
%{key: "relay", label: "Server host", type: :string, required: true},
%{key: "port", label: "Port", type: :integer, default: 587},
%{key: "username", label: "Username", type: :string},
%{key: "password", label: "Password", type: :secret}
]
},
%{
key: "postmark",
name: "Postmark",
module: Swoosh.Adapters.Postmark,
fields: [
%{key: "api_key", label: "API key", type: :secret, required: true}
]
},
# ... etc
]
end
end
The UI renders dynamically from this registry. Secret fields use put_secret/2 and show masked hints via secret_hint/1. Non-secret fields use put_setting/3.
Storage in Settings table
Uses the existing key-value Settings table:
| Key | Type | Example |
|---|---|---|
email_adapter |
string | "postmark" |
email_relay |
string | "smtp.example.com" |
email_port |
integer | 587 |
email_api_key |
encrypted | (encrypted via Vault) |
email_password |
encrypted | (encrypted via Vault) |
email_username |
string | "user@example.com" |
email_domain |
string | "mg.example.com" |
All keys are prefixed with email_. Only the fields relevant to the selected adapter are written; others are cleared on adapter change.
Runtime config loading
On app startup and on settings save:
- Read
email_adapterfrom Settings - Look up adapter in registry
- Read all config fields for that adapter from Settings
- Build Swoosh config keyword list
- Call
Application.put_env(:berrypod, Berrypod.Mailer, config)
This is wrapped in a Berrypod.Mailer.load_config/0 function, called from:
Application.start/2(boot)- The admin settings save handler (runtime update without restart)
Updating email_configured?/0
The helper from setup-auto-confirm.md already checks if the adapter is not Swoosh.Adapters.Local. No changes needed — once the admin configures an adapter via the UI, Application.get_env returns the new adapter and the function returns true.
Changes
1. Adapter registry (lib/berrypod/mailer/adapters.ex — new)
Module with all/0, get/1, field_keys/1 functions. Pure data, no side effects.
2. Config loader (lib/berrypod/mailer.ex)
Add to existing module:
load_config/0— reads from Settings, builds Swoosh config, callsApplication.put_env/3env_var_configured?/0— returns true ifSMTP_HOSTis set (used by UI to show read-only state)current_config/0— returns the active adapter name + config for display in the UI
3. Load on boot (lib/berrypod/application.ex)
Call Mailer.load_config/0 after Repo is started (needs DB access for Settings reads and secret decryption).
4. Admin email settings page (lib/berrypod_web/live/admin/email_settings.ex — new)
LiveView at /admin/settings/email:
- Adapter dropdown (from registry)
- Dynamic form fields based on selected adapter
- Secret fields show masked hints when a value exists, clear button, new value input
- "Send test email" button — sends a test email to the admin's address
- Read-only mode when env vars are active (show values from env, disable form)
- Save persists to Settings table, calls
Mailer.load_config/0to apply immediately
5. Route (lib/berrypod_web/router.ex)
Add in the authenticated admin live_session:
live "/admin/settings/email", Admin.EmailSettings
6. Link from warning banner
Update the warning banner from setup-auto-confirm to link to /admin/settings/email:
"Email delivery not configured. Configure email ->"
7. Admin nav
Add "Email" under settings in the admin sidebar, or as a tab on the existing settings page.
8. Test email function (lib/berrypod/mailer.ex)
send_test_email/1— sends a simple test email to the given address using current config. Returns{:ok, _}or{:error, reason}.
9. Tests
test/berrypod/mailer_test.exs (new):
load_config/0reads from Settings and applies to Application envload_config/0with no settings keeps Local adapterenv_var_configured?/0checks SMTP_HOST
test/berrypod/mailer/adapters_test.exs (new):
- Registry returns all adapters
- Each adapter has required fields
test/berrypod_web/live/admin/email_settings_test.exs (new):
- Page renders adapter dropdown
- Changing adapter shows correct fields
- Saving config persists to Settings
- Secret fields show masked hints
- Read-only when env vars are set
Files to modify
| File | Change |
|---|---|
lib/berrypod/mailer/adapters.ex |
New — adapter registry |
lib/berrypod/mailer.ex |
Add load_config/0, env_var_configured?/0, current_config/0, send_test_email/1 |
lib/berrypod/application.ex |
Call Mailer.load_config/0 on boot |
lib/berrypod_web/live/admin/email_settings.ex |
New — admin settings page |
lib/berrypod_web/router.ex |
Add email settings route |
lib/berrypod_web/components/layouts/admin.html.heex |
Link banner to settings page, add nav entry |
assets/css/admin/components.css |
Styles for email settings form |
| Tests (3 new files) | Mailer, adapters, LiveView |
Verification
mix precommitpasses- No env vars set: admin UI at
/admin/settings/emailshows adapter dropdown, can configure Postmark with API key, save,email_configured?()returns true, warning banner disappears - With
SMTP_HOSTset: admin UI shows SMTP config read-only with "controlled by environment variables" note - "Send test email" delivers to admin's address
- Server restart: config reloaded from DB, email still works
- Adapter change: old config cleared, new adapter's fields shown, save applies immediately