Mailglass stores tenant_id on deliveries, events, and suppressions from day one. Phase 26 adds runtime per-tenant outbound adapter resolution without changing the zero-config single-tenant path.

Single-tenant default

If you only have one transport, keep the default path boring:

config :mailglass,
  adapter:
    {Mailglass.Adapters.Swoosh,
     swoosh_adapter:
       {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_API_KEY")}}

You do not need config :mailglass, adapters: or a custom tenancy callback for this case. Mailglass.Tenancy.SingleTenant keeps returning :default, and queued deliveries persist the reserved default adapter_ref internally so retries stay deterministic.

Named adapter refs

Add config :mailglass, adapters: only when you need reusable runtime route targets:

config :mailglass,
  adapter:
    {Mailglass.Adapters.Swoosh,
     swoosh_adapter:
       {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_DEFAULT_API_KEY")}},
  adapters: [
    postmark_acme:
      {Mailglass.Adapters.Swoosh,
       swoosh_adapter:
         {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_ACME_API_KEY")}},
    sendgrid_globex:
      {Mailglass.Adapters.Swoosh,
       swoosh_adapter:
         {Swoosh.Adapters.Sendgrid, api_key: System.fetch_env!("SENDGRID_GLOBEX_API_KEY")}},
    ses_ops:
      {Mailglass.Adapters.Swoosh,
       swoosh_adapter:
         {Swoosh.Adapters.AmazonSES, region: "us-east-1", access_key: System.fetch_env!("SES_ACCESS_KEY"), secret: System.fetch_env!("SES_SECRET")}}
  ]

Each registry entry is just the same adapter shape Mailglass already understands: AdapterModule or {AdapterModule, opts}.

Tenancy callback

Put routing policy on your existing Mailglass.Tenancy module with resolve_outbound_adapter_ref/1:

defmodule MyApp.Tenancy do
  @behaviour Mailglass.Tenancy

  @impl Mailglass.Tenancy
  def scope(query, %{tenant_id: tenant_id}) do
    Mailglass.Tenancy.scope(query, %{tenant_id: tenant_id})
  end

  @impl Mailglass.Tenancy
  def resolve_webhook_tenant(%{path_params: %{"tenant_id" => tenant_id}}), do: {:ok, tenant_id}
  def resolve_webhook_tenant(_ctx), do: {:error, :missing_tenant_id}

  @impl Mailglass.Tenancy
  def resolve_outbound_adapter_ref(%{tenant_id: tenant_id, message: message, mode: mode}) do
    case {tenant_id, message.stream, mode} do
      {"acme", :transactional, _mode} -> {:ok, :postmark_acme}
      {"globex", :transactional, _mode} -> {:ok, :sendgrid_globex}
      {"initech", :operational, :async} -> {:ok, :ses_ops}
      _ -> :default
    end
  end
end

The callback contract stays narrow on purpose:

  • {:ok, adapter_ref} selects a named route from config :mailglass, adapters:
  • :default keeps the global config :mailglass, adapter path
  • missing callbacks behave the same as :default

Broken callback output or unknown refs fail loudly. Mailglass does not silently fall back to the default adapter when tenant-specific routing is misconfigured.

Common routing patterns

Different ESP per tenant

Route one tenant through Postmark and another through SendGrid by returning different named refs from resolve_outbound_adapter_ref/1.

Same ESP family, different credentials

Point multiple refs at the same adapter module with different API keys or subaccount options:

config :mailglass, adapters: [
  acme_postmark:
    {Mailglass.Adapters.Swoosh,
     swoosh_adapter:
       {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_ACME_API_KEY")}},
  globex_postmark:
    {Mailglass.Adapters.Swoosh,
     swoosh_adapter:
       {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_GLOBEX_API_KEY")}}
]

Same provider family, different stream or domain routes

Use route refs to separate transactional and operational traffic even when the provider family is the same:

config :mailglass, adapters: [
  ses_transactional:
    {Mailglass.Adapters.Swoosh,
     swoosh_adapter:
       {Swoosh.Adapters.AmazonSES, region: "us-east-1", configuration_set_name: "transactional"}},
  ses_bulk:
    {Mailglass.Adapters.Swoosh,
     swoosh_adapter:
       {Swoosh.Adapters.AmazonSES, region: "us-east-1", configuration_set_name: "bulk"}}
]

Sync vs async semantics

  • Mailglass.deliver/2 resolves the effective adapter at send time.
  • Mailglass.deliver_later/2 and Mailglass.deliver_many/2 persist delivery.adapter_ref before the job is enqueued.
  • Worker dispatch resolves credentials from runtime config at execution time, but it does not rerun tenant routing. That keeps retries on the same named route even if your tenancy callback would choose something different later.

Queued paths should use adapter_ref overrides, not raw adapter tuples. Mailglass will reject queued raw adapter overrides that cannot be persisted safely without storing secrets.