berrypod/docs/plans/email-settings.md
jamey 9d9bd09059
Some checks failed
deploy / deploy (push) Has been cancelled
auto-confirm admin during setup, skip email verification
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>
2026-02-21 10:24:26 +00:00

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:

  1. Read email_adapter from Settings
  2. Look up adapter in registry
  3. Read all config fields for that adapter from Settings
  4. Build Swoosh config keyword list
  5. 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, calls Application.put_env/3
  • env_var_configured?/0 — returns true if SMTP_HOST is 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/0 to apply immediately

5. Route (lib/berrypod_web/router.ex)

Add in the authenticated admin live_session:

live "/admin/settings/email", Admin.EmailSettings

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/0 reads from Settings and applies to Application env
  • load_config/0 with no settings keeps Local adapter
  • env_var_configured?/0 checks 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

  1. mix precommit passes
  2. No env vars set: admin UI at /admin/settings/email shows adapter dropdown, can configure Postmark with API key, save, email_configured?() returns true, warning banner disappears
  3. With SMTP_HOST set: admin UI shows SMTP config read-only with "controlled by environment variables" note
  4. "Send test email" delivers to admin's address
  5. Server restart: config reloaded from DB, email still works
  6. Adapter change: old config cleared, new adapter's fields shown, save applies immediately