Defaults plugin
Merge org-wide headers, tags, reply-to, and idempotency key prefixes into every send — with message values winning.
defaultsPlugin injects shared values into every message before validation, so org-wide headers, tags, and a reply-to address live in one place instead of every call site. Per-message values always take precedence over the defaults.
import { createEmailClient } from "@opencoredev/email-sdk";
import { defaultsPlugin } from "@opencoredev/email-sdk/plugins/defaults";
import { resend } from "@opencoredev/email-sdk/resend";
export const email = createEmailClient({
adapters: [resend({ apiKey: process.env.RESEND_API_KEY! })],
plugins: [
defaultsPlugin({
headers: { "X-Entity-Ref": "acme-app" },
tags: [{ name: "env", value: process.env.NODE_ENV! }],
replyTo: "support@acme.com",
idempotencyKeyPrefix: "acme:",
}),
],
});
// Every send now carries the header, the env tag, and the reply-to —
// without repeating them in application code:
await email.send({
from: "Acme <hello@acme.com>",
to: "user@example.com",
subject: "Welcome",
text: "Your account is ready.",
});Options
Prop
Type
Merge semantics
Each option has an exact, predictable rule:
| Option | Rule |
|---|---|
headers | Merged by name; a message header with the same name wins over the default. |
metadata | Merged by key; message metadata wins. |
sendMetadata | Merged into options.metadata; per-send metadata wins. |
tags | Concatenated — defaults first, then message tags. Nothing is replaced. |
replyTo | Applied only when the message has no replyTo at all. |
idempotencyKey | Applied only when neither the message nor send options carry a key. |
idempotencyKeyPrefix | Prepended to whichever key ends up applying — unless the key already starts with the prefix, so re-runs never double-prefix. |
So with the client above, a message that sets headers: { "X-Entity-Ref": "checkout" } sends checkout (message wins), while its tags gain the env tag in front of its own.
Defaults are validated like any other field
Defaults merge before adapter validation. If a default adds a field the selected route cannot
represent — tags on SMTP, for example — the send throws an EmailValidationError before any
request. Check field support when adding default tags or
metadata to a client with fallback routes.
Stack layers
Mount multiple defaults plugins with distinct ids; they apply in array order, so later layers see the merged result of earlier ones:
plugins: [
defaultsPlugin({ id: "defaults:org", headers: { "X-Org": "acme" } }),
defaultsPlugin({ id: "defaults:billing", tags: [{ name: "team", value: "billing" }] }),
];