Email

Resend

Deliver VitNode email through Resend's HTTPS API - install the adapter, add a key, verify the domain, watch the first mail land.

Resend sends mail over an ordinary HTTPS request, which makes it the adapter that works everywhere your app can fetch - including serverless and edge deployments where an SMTP connection would never open.

CloudSelf-HostedLinks
✅ Supported✅ SupportedNPM Package · Resend docs

Quick start

src/vitnode.api.config.ts
import { buildApiConfig } from '@vitnode/core/vitnode.config'
import { ResendEmailAdapter } from '@vitnode/resend'

export const vitNodeApiConfig = buildApiConfig({
  email: {
    adapter: ResendEmailAdapter({
      apiKey: process.env.RESEND_API_KEY,
      from: process.env.RESEND_FROM_EMAIL,
    }),
  },
})

That is the whole integration. The steps below are the account setup around it.

Setup

Install the adapter

The resend SDK ships as a dependency of the adapter, so this is the only package you add.

Install the Resend adapter
bun i @vitnode/resend
pnpm i @vitnode/resend
npm i @vitnode/resend

Create an API key

In the Resend dashboard, open API Keys and create one with send permission. The value starts with re_ and is shown exactly once, so copy it now.

Verify a sending domain

Resend only delivers from a domain you own. Add yours under Domains, copy the DKIM and SPF records it gives you into your DNS, and wait for the status to go green.

For a first local test you can skip this and send from onboarding@resend.dev, which Resend allows for testing only - it will not carry your production mail.

Register the adapter

Add the email block to your API config. Both values come from the environment, so nothing secret lives in the repository:

src/vitnode.api.config.ts
import { buildApiConfig } from '@vitnode/core/vitnode.config'
import { ResendEmailAdapter } from '@vitnode/resend'

export const vitNodeApiConfig = buildApiConfig({
  email: {
    adapter: ResendEmailAdapter({
      apiKey: process.env.RESEND_API_KEY,
      from: process.env.RESEND_FROM_EMAIL,
    }),
  },
  metadata: {
    title: 'My Community',
    shortTitle: 'Community',
  },
})

Recipients see Community <hello@example.com>: the display name is metadata.shortTitle ?? metadata.title, and from supplies only the address.

Set the environment variables

.env
RESEND_API_KEY=re_your_api_key
RESEND_FROM_EMAIL=hello@your-verified-domain.com

Send a test mail

Send one from a route - the email overview has the build() plus deliver() form that works from any plugin. Because deliver() runs inside the request, a rejection fails that request: the adapter rethrows Resend's error as [error_name]: message, which is the response body in development and a log line in production.

An accepted message appears under Emails in the dashboard within seconds, with its delivery events.

Mail sent with the queued send() - core's password resets, for instance - is one tick behind, and the row tells you where it got to:

select status, attempts, "lastError"
from core_queue
where name = 'send-email'
order by id desc
limit 1;

Options

Prop

Type

Environment variables

Both are read in src/vitnode.api.config.ts, so the names are yours to choose; these are the ones this documentation uses.

VariableMaps toExampleNotes
RESEND_API_KEYapiKeyre_123...Created under API Keys. Needs sending permission
RESEND_FROM_EMAILfromhello@your-domain.comMust sit on a verified domain, or be onboarding@resend.dev while testing

Gotchas

Missing keys fail at delivery, not at boot

The adapter validates its own config inside sendEmail, so an unset RESEND_API_KEY never stops the API from starting. With deliver() the failure is the route's own 500, throwing Missing Resend configuration. With the queued send() the request still returns 200 and the same message turns up in core_queue.lastError three attempts later.

An unverified From fails at delivery

Resend answers with an error rather than holding the mail, and the adapter rethrows it as [error_name]: message. If sends start failing the moment you switch from onboarding@resend.dev to your own domain, check the domain status before you check your code.

Only the HTML part is sent

VitNode renders both an HTML and a plain-text version of every email, but this adapter passes html alone - the text argument reaches it and goes unused.

Next