Logo VitNode
Documentation

Email

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

apps/api/src/lib/email/postmark-adapter.ts
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:

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 } 
},

Learn More