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
endSecure 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
endReturn 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.metais a map containing relevant provider response details (like an external message ID). It will be persisted tochimeway_delivery_attempts.provider_response.- Security Note: You MUST redact sensitive fields (like passwords, tokens, secrets, API keys, or auth headers) from
metabefore returning it.
Error
{:error, class, detail}: Indicates the delivery failed.classMUST 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.
detailis a map containing error specifics. Likemeta, 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 ...
endUsing this macro is the recommended way to ensure your adapter meets Chimeway's production-grade standards.