Test transactional email locally
Send a transactional email with Resend's real Node SDK, inspect its content locally and resume the same message after stopping the World. This example needs Node 22.6 or newer, npm and registry access during installation. It needs no Resend account, real API key or platform login. No email leaves the World or reaches a real mailbox.
Start in an empty directory. Save the following files there.
Install and select the twin
{
"name": "local-email-example",
"private": true,
"type": "module",
"dependencies": { "resend": "6.6.0" },
"devDependencies": {
"@volter/world": "3.0.69",
"@volter/twin-resend": "1.0.1"
}
}RESEND_API_KEY=npm install
npx volter world init --name local-email-example --twins resend --source resend=@volter/twin-resendReview the selected vendor and .volter/world.json: the service should name
@volter/twin-resend version 1.0.1. The World supplies a throwaway API key. Commit the config
and lockfile to keep that choice explicit. Choose a twin
explains how to compare a release's supported operations and exercised coverage.
Send with the unchanged SDK
The twin's default account owns [email protected]. Like Resend's testing domain,
resend.dev sends only to the account owner. Use that synthetic recipient for this example.
An app using its own sender domain needs a verified domain in the World;
the release's README describes its local DNS setup.
import assert from 'node:assert/strict';
import { writeFileSync } from 'node:fs';
import { Resend } from 'resend';
assert.ok(process.env.VOLTER_WORLD, 'run this script through the World');
const resend = new Resend(process.env.RESEND_API_KEY);
const email = {
from: 'Example app <[email protected]>',
to: ['[email protected]'],
subject: 'Your local sign-in code',
text: 'Your synthetic sign-in code is 123456.',
html: '<p>Your synthetic sign-in code is <strong>123456</strong>.</p>'
};
const sent = await resend.emails.send(email);
assert.equal(sent.error, null);
assert.ok(sent.data.id);
const saved = await resend.emails.get(sent.data.id);
assert.equal(saved.error, null);
assert.equal(saved.data.id, sent.data.id);
assert.equal(saved.data.subject, email.subject);
assert.equal(saved.data.text, email.text);
assert.deepEqual(saved.data.to, email.to);
writeFileSync('last-email.json', JSON.stringify({ id: sent.data.id }) + '\n');
console.log('email sent and read back locally');npx volter world up
npx volter world clock set 2026-01-15T12:00:00Z
npx volter world run -- node send-email.mjsemail sent and read back locallyThe SDK uses its ordinary destination and the World's credential. Injection routes it to the twin. The returned ID names a stored email; a scenario handler does not invent this write.
Inspect the local inbox
The twin models delivery five seconds after sending. Advance its clock rather than sleeping.
/_twin/mail is a twin-specific inbox endpoint, not a Resend production API.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { Resend } from 'resend';
assert.ok(process.env.VOLTER_WORLD, 'run this script through the World');
const { id } = JSON.parse(readFileSync('last-email.json', 'utf8'));
const resend = new Resend(process.env.RESEND_API_KEY);
const saved = await resend.emails.get(id);
assert.equal(saved.error, null);
assert.equal(saved.data.last_event, 'delivered');
const response = await fetch(`${process.env.RESEND_TWIN_URL}/_twin/mail?to=owner%40world.test`);
assert.equal(response.status, 200);
const inbox = await response.json();
const email = inbox.mail.find((message) => message.id === id);
assert.ok(email, 'the delivered message must be in the local inbox');
assert.equal(email.subject, 'Your local sign-in code');
assert.match(email.text, /123456/);
assert.match(email.html, /<strong>123456<\/strong>/);
console.log('local inbox contains the delivered email and sign-in code');npx volter world clock advance 5s
npx volter world run -- node inspect-email.mjs
npx volter world logThe inbox assertions check stored content and simulated delivery. They do not measure real deliverability, spam filtering or email-client rendering. An existing app can use the same inbox endpoint to retrieve its sign-in link or code during a local workflow.
Stop and resume the same email
npx volter world down
npx volter world up
npx volter world run -- node inspect-email.mjs
npx volter world downlocal inbox contains the delivered email and sign-in code
Stopped local-email-exampledown stops compute and retains the message. A later up resumes its state. Keep
last-email.json while reproducing this example; it records which message to inspect.
For a fresh run, reset deliberately, freeze the clock,
send again and inspect the newly returned ID.
For failures such as a rate limit, use scenario handlers. They author refusals and stateless behavior; successful email sends still create real local records. The catalog release describes its other supported operations, local setup endpoints and gaps.