Plugin API
Type reference for EmailPlugin, EmailPluginContext, EmailSendMiddleware events, and typed client extensions.
Reference for the plugin surface. For patterns and guidance, start with Writing plugins.
EmailPlugin
type EmailPlugin<TExtension extends object = object> = {
id: string;
adapters?: EmailProvider[] | ((ctx: EmailPluginContext) => EmailProvider[]);
hooks?: EmailHooks;
middleware?: EmailSendMiddleware[];
extendClient?: (ctx: EmailPluginContext) => TExtension;
};Prop
Type
createEmailClient is synchronous: it registers direct adapters first, then processes plugins in array order, then applies extendClient extensions after the client object is built.
EmailPluginContext
Passed to adapters factories and extendClient.
type EmailPluginContext = {
adapters: ReadonlyMap<string, EmailProvider>;
defaultAdapter: string;
addAdapter(adapter: EmailProvider): void;
};Prop
Type
If an adapters factory both calls ctx.addAdapter(adapter) and returns the same adapter object, it registers once.
EmailSendMiddleware
type EmailSendMiddleware = {
beforeSend?: (event: EmailBeforeSendEvent) => MaybePromise<EmailBeforeSendResult | void>;
afterSend?: (event: EmailAfterSendEvent) => MaybePromise<void>;
onError?: (event: EmailErrorEvent) => MaybePromise<void>;
};| Callback | Runs | Errors |
|---|---|---|
beforeSend | Once per send, before validation and adapter routing. | Stop the send |
afterSend | After a provider returns a normalized response. | Swallowed |
onError | After an adapter route fails its last retry, before any fallback. | Swallowed |
beforeSend
type EmailBeforeSendEvent = { message: EmailMessage; options?: SendOptions };
type EmailBeforeSendResult = { message?: EmailMessage; options?: SendOptions };No provider field — routing has not happened yet. Returning a result transforms the send: message replaces the message wholesale, options shallow-merges over the current send options. Return nothing to observe or to throw for policy. Middleware runs in plugin order, each seeing the previous one's result.
afterSend and onError
Both extend the hook event shape:
type EmailAfterSendEvent = {
provider: string;
message: EmailMessage;
attempt: number;
metadata?: Record<string, unknown>;
response: EmailProviderResponse;
};
type EmailErrorEvent = {
provider: string;
message: EmailMessage;
attempt: number;
metadata?: Record<string, unknown>;
error: unknown;
};After onError, fallback adapters may still run — it signals a failed route, not necessarily a failed send.
Typed client extensions
EmailPlugin<TExtension> carries the extension type, and createEmailClient infers the result from the plugins array:
// EmailClient<{ capture: EmailCaptureStore }>
const email = createEmailClient({
adapters: [memoryProvider()],
plugins: [capturePlugin()],
});Helper types behind the inference:
Prop
Type
Multiple extending plugins intersect: the client gains every extension, and a runtime key collision throws EmailValidationError.
Registration errors
All thrown by createEmailClient as EmailValidationError:
| Condition | Message |
|---|---|
| Duplicate plugin id | Duplicate email plugin "<id>". |
| Duplicate adapter name | Duplicate email adapter "<name>". |
| Async adapters factory | Email plugin "<id>" returned async adapters. createEmailClient requires synchronous plugin adapters. |
| Extension key collision | Email plugin "<id>" tried to extend the client with reserved key "<key>". |
