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.
| Cloud | Self-Hosted | Links |
|---|---|---|
| ✅ Supported | ✅ Supported | NPM Package · Resend docs |
Quick start
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.
bun i @vitnode/resendpnpm i @vitnode/resendnpm i @vitnode/resendCreate 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:
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
RESEND_API_KEY=re_your_api_key
RESEND_FROM_EMAIL=hello@your-verified-domain.comSend 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.
| Variable | Maps to | Example | Notes |
|---|---|---|---|
RESEND_API_KEY | apiKey | re_123... | Created under API Keys. Needs sending permission |
RESEND_FROM_EMAIL | from | hello@your-domain.com | Must 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.