Build a production send pipeline
Validate before queueing, preserve durable state, and use retries and fallback without hiding delivery uncertainty.
The core SDK owns one process-local delivery attempt. Your application owns durable queueing, persisted idempotency, and recovery after process failure.
Construct explicit routes
import { createEmailClient } from "@opencoredev/email-sdk";
import { resend } from "@opencoredev/email-sdk/resend";
import { smtp } from "@opencoredev/email-sdk/smtp";
export const email = createEmailClient({
adapters: [
resend({ apiKey: process.env.RESEND_API_KEY! }),
smtp({
name: "backup",
host: process.env.SMTP_HOST!,
auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
}),
],
defaultAdapter: "resend",
retry: { maxAttempts: 2 },
fallback: {
adapters: ["backup"],
onUnknownDelivery: "stop",
},
});Choose the backup from the field support and capability tables. SMTP supports attachments, but has no tag, metadata, or scheduling concept.
Validate before accepting work
Use the same public no-network boundary as send before committing a job to your queue.
await email.validate(message, {
fallback: { adapters: ["backup"] },
});
await queue.add("send-email", { message, idempotencyKey });Send with a stable idempotency key
await email.send(job.message, {
idempotencyKey: job.idempotencyKey,
metadata: { jobId: job.id },
signal: job.signal,
});The key is effective only where the selected adapter supports it. Persist your own business-level deduplication before invoking the SDK.
Handle delivery certainty
- Retry
not_sentfailures whenretryableis true. - Let configured fallback advance after terminal
not_sentfailures. - Stop on
unknownby default and reconcile provider state before retrying.onUnknownDelivery: "stop"only controls fallback to the next adapter; same-adapter retries followretry, so setretry: { maxAttempts: 1 }when a durable worker owns retries. - Opt into
onUnknownDelivery: "continue"only when a duplicate is safer than a missed message.
Record safe facts
Use hooks or observabilityPlugin for adapter, attempt, stable error code, duration, and redacted message facts. Avoid bodies, addresses, credentials, idempotency keys, provider response bodies, URLs, and filesystem paths.
Use a durable integration when needed
The durable PostgreSQL example is a runnable version of this guide: idempotent enqueue, leased worker claims, a needs_reconciliation state for unknown outcomes, verified webhook ingestion with replay protection, and a suppression table.
Convex Email provides queue state, attempts, cancellation, test-mode redirection, and webhook history for Convex applications.
Troubleshoot failures
Inspect typed errors and delivery classification.