Adapter model
How adapters register routing names, map messages to provider APIs, and expose escape hatches.
An adapter is a module that knows how to send one normalized EmailMessage through one service or transport. The client holds a registry of adapters keyed by routing name; everything else — defaults, fallbacks, per-send overrides — is built on those names.
import { createEmailClient } from "@opencoredev/email-sdk";
import { resend } from "@opencoredev/email-sdk/resend";
import { postmark } from "@opencoredev/email-sdk/postmark";
const email = createEmailClient({
adapters: [
resend({ apiKey: process.env.RESEND_API_KEY! }),
postmark({ serverToken: process.env.POSTMARK_SERVER_TOKEN! }),
],
// defaultAdapter is optional — it falls back to the first adapter ("resend" here)
});Each adapter import is a separate entry point (@opencoredev/email-sdk/resend, /postmark, ...), so your bundle only contains the providers you actually send through.
The adapter contract
Every adapter does three things:
- Maps supported
EmailMessagefields to the provider's API payload. - Rejects unsupported fields with an
EmailValidationErrorbefore any request — never silently drops data. See field support. - Normalizes the result into
{ provider, id?, messageId?, accepted?, rejected?, raw? }and provider failures into typed errors with retryability info.
The full interface for building your own is in the adapter contract reference.
Routing names
Adapter factories register fixed routing names — resend, postmark, sendgrid, cloudflare, unosend, ses, mailgun, mailersend, brevo, mailchimp, sparkpost, iterable, loops, sequenzy, plunk, mailtrap, scaleway, zeptomail, mailpace, and smtp (the SMTP factory accepts a name option for running several SMTP routes side by side).
Those names are what you pass everywhere a route is selected:
// per-send override
await email.send(message, { adapter: "postmark" });
// client-level default and fallback order
createEmailClient({ adapters, defaultAdapter: "resend", fallback: ["postmark"] });
// a client pinned to one route
const transactional = email.withAdapter("postmark");
await transactional.send(message);Selecting a name that is not registered throws EmailProviderNotFoundError — routes are explicit, never guessed.
Escape hatches
Adapters can expose provider-specific extras through raw. Use it to keep provider-coupled code next to the adapter while application code stays on the shared send path:
const adapter = email.adapter("resend");
console.log(adapter.raw); // { baseUrl: "https://api.resend.com" }The memory test adapter uses the same hatch to expose its sent array.
Naming aliases
adapter and provider mean the same thing throughout the API. providers, defaultProvider, fallbackProviders, email.provider(), and email.withProvider() are aliases kept for compatibility — new code should prefer the adapter spellings.
