Custom Email Adapter
Build a custom VitNode email adapter for any third-party provider like Postmark, AWS SES, or Mailgun.
You can connect any transactional email service (e.g. Postmark, AWS SES, SendGrid, Mailgun) by implementing the EmailApiPlugin interface. VitNode handles template rendering, i18n localization, and recipient resolution; your adapter just sends the email.
The EmailApiPlugin Interface
The adapter contract consists of a single sendEmail method:
export interface EmailApiPlugin {
sendEmail: (args: {
to: string
subject: string
html: string
text: string
metadata: { title: string; shortTitle?: string }
replyTo?: string
idempotencyKey?: string
}) => Promise<EmailSendResult | void>
}
export interface EmailSendResult {
id?: string // the provider's message id
}Quick start: Implementing an Adapter
1. Create the Adapter Factory
import type { EmailApiPlugin } from "@vitnode/core/api/models/email"
export const PostmarkEmailAdapter = ({
serverToken,
from,
}: {
serverToken: string
from: string
}): EmailApiPlugin => ({
sendEmail: async ({ to, subject, html, text, metadata }) => {
const res = await fetch("https://api.postmarkapp.com/email", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Postmark-Server-Token": serverToken,
},
body: JSON.stringify({
From: `${metadata.shortTitle ?? metadata.title} <${from}>`,
To: to,
Subject: subject,
HtmlBody: html,
TextBody: text,
}),
})
if (!res.ok) throw new Error(`Postmark error: ${res.statusText}`)
},
})2. Register in API Configuration
Attach the adapter to vitNodeApiConfig in apps/api/src/vitnode.api.config.ts:
import { buildApiConfig } from "@vitnode/core/vitnode.config"
import { PostmarkEmailAdapter } from "./lib/email/postmark-adapter"
export const vitNodeApiConfig = buildApiConfig({
email: {
adapter: PostmarkEmailAdapter({
serverToken: process.env.POSTMARK_SERVER_TOKEN!,
from: "notifications@yourdomain.com",
}),
},
})sendEmail Arguments
Prop
Type
Idempotency and the Return Value
Callers such as notification emails retry a send that failed or never got an answer, with the same idempotencyKey. Forward it to a provider that deduplicates on a key, and a retry after an accepted-but-unconfirmed send is dropped instead of delivered twice. Resend does this; SMTP cannot, so Nodemailer ignores the key.
Return { id } with the provider's message id when you have one - it shows up in the AdminCP delivery list. Returning nothing is still valid.
sendEmail: async ({ to, subject, html, text, metadata, idempotencyKey }) => {
const res = await fetch("https://api.postmarkapp.com/email", {
/* ...as above, plus your provider's idempotency field if it has one */
})
if (!res.ok) throw new Error(`Postmark error: ${res.statusText}`)
const { MessageID } = (await res.json()) as { MessageID: string }
return { id: MessageID }
},