Custom Adapter Recipe

Copy Markdown View Source

This guide walks you through implementing a custom outbound channel adapter using the Chimeway.Adapter behaviour. It covers required callbacks, secure runtime configuration, returning delivery outcomes, and writing contract tests to enforce redaction.

Implementing the Behaviour

A custom adapter in Chimeway is any module that implements the Chimeway.Adapter behaviour. The primary responsibility of an adapter is to take a pre-planned %Chimeway.Delivery{} struct and dispatch it to an external provider (like an SMS gateway, email service, or push notification API).

Here is a basic structure for a custom adapter:

defmodule MyApp.MyCustomAdapter do
  @behaviour Chimeway.Adapter

  @impl true
  def deliver(%Chimeway.Delivery{} = delivery, config) do
    # Implementation goes here
  end
end

Secure Runtime Configuration

CRITICAL: Adapter configuration (such as API keys, base URLs, or secrets) MUST be loaded at runtime.

Do not use compile-time module attributes (e.g., @api_key Application.compile_env(...)). Instead, you must read configuration at call time, typically via Application.get_env/3 or through the config keyword list passed to your deliver/2 function.

Loading configuration at runtime ensures multi-environment safety and allows you to seamlessly switch environments or override configurations during tests without needing to recompile your application.

defmodule MyApp.MyCustomAdapter do
  @behaviour Chimeway.Adapter

  @impl true
  def deliver(%Chimeway.Delivery{} = delivery, config) do
    # Good: Reading config at runtime
    api_key = Keyword.get(config, :api_key) || Application.get_env(:my_app, :custom_adapter_api_key)
    
    # ... external API call ...
  end
end

Return Shapes and Outcomes

The deliver/2 function must return one of the following specific shapes to allow Chimeway's dispatcher to properly handle the outcome:

Success

  • {:ok, meta}: Indicates the provider accepted the delivery.
    • meta is a map containing relevant provider response details (like an external message ID). It will be persisted to chimeway_delivery_attempts.provider_response.
    • Security Note: You MUST redact sensitive fields (like passwords, tokens, secrets, API keys, or auth headers) from meta before returning it.

Error

  • {:error, class, detail}: Indicates the delivery failed.
    • class MUST be one of :temporary, :permanent, or :bounced:
      • :temporary - A transient failure (e.g., 500 Server Error). The dispatcher may retry.
      • :permanent - A non-retriable rejection (e.g., 400 Bad Request).
      • :bounced - The address or identity is unreachable.
    • detail is a map containing error specifics. Like meta, it must not contain any sensitive data, PII, or full provider response bodies.

Enforcing Safety with Contract Tests

To guarantee that your custom adapter correctly implements the behaviour and adheres to security requirements (specifically, redaction of credentials in metadata), Chimeway provides a shared contract test suite.

You should use the Chimeway.Adapter.ContractTest macro in your test suite to automatically run these checks against your adapter. This enforces runtime safety and prevents accidental credential leaks.

defmodule MyApp.MyCustomAdapterTest do
  use ExUnit.Case, async: true
  
  # This macro injects shared contract tests for your adapter
  use Chimeway.Adapter.ContractTest, adapter: MyApp.MyCustomAdapter

  # ... your custom tests ...
end

Using this macro is the recommended way to ensure your adapter meets Chimeway's production-grade standards.