import { RelayClient } from '@insureco/relay'
const relay = RelayClient.fromEnv() // reads BIO_CLIENT_ID + BIO_CLIENT_SECRET
// Fire-and-forget — NEVER await in request handlers
relay.sendEmail({
template: 'welcome',
to: { email: user.email, name: user.name },
data: { firstName: user.firstName, orgName: org.name },
}).catch((err) => logger.warn({ err }, 'Welcome email failed'))
RelayClient.fromEnv() auto-detects:
RELAY_DIRECT_URL set → local dev (direct to relay, bypasses Janus)BIO_CLIENT_ID + BIO_CLIENT_SECRET set → production (auto-injected by builder)JWT_SECRET + PROGRAM_ID set → legacy, deprecated — migrate to option 2No internalDependencies needed in catalog-info.yaml. Relay is a platform service accessible from all services through Janus automatically.
For local dev, add to .env.local:
RELAY_DIRECT_URL=http://localhost:4001
Email and SMS are never on the critical path. A relay outage must not break your API.
// ✅ CORRECT: fire-and-forget
relay.sendEmail({ ... }).catch((err) => logger.warn({ err }, 'Email failed'))
// ❌ WRONG: awaiting blocks your response
await relay.sendEmail({ ... }) // your API times out if relay is slow
Always include .catch(). Silent failures mean lost audit events.
await relay.sendEmail({
template: 'welcome', // see Built-In Templates below
to: { email: '[email protected]', name: 'Jane Doe' },
data: { firstName: 'Jane', orgName: 'Acme Corp' },
})
relay.sendEmail({
content: {
subject: 'Your quote is ready',
html: '<h1>Hello {{name}},</h1><p>Your quote is attached.</p>',
text: 'Hello {{name}}, your quote is ready.', // plain text fallback
},
to: { email: '[email protected]', name: 'Jane' },
data: { name: 'Jane' },
options: {
fromName: 'MyProgram', // display name only, NOT the from address
cc: ['[email protected]'],
bcc: '[email protected]',
},
}).catch((err) => logger.warn({ err }, 'Quote email failed'))
relay.sendSMS({
content: { text: 'Your code is {{code}}. Expires in 10 minutes.' },
to: { phone: '+15551234567' }, // E.164 format required: +{country}{number}
data: { code: '123456' },
}).catch((err) => logger.warn({ err }, 'SMS failed'))
| Name | Required Data |
|---|---|
welcome | firstName, orgName |
invitation | inviterName, orgName, inviteUrl |
password-reset | firstName, resetUrl |
magic-link | firstName, magicUrl |
relay.sendEmail({
content: { subject: 'Policy bound', html: '...' },
to: { email: '[email protected]' },
options: {
cc: ['[email protected]'], // visible to all recipients
bcc: '[email protected]', // hidden, max 50 addresses each
},
})
relay.sendEmail({
template: 'invitation',
to: { email: invitee.email },
data: { inviterName: user.name, orgName: org.name, inviteUrl },
metadata: {
sourceService: 'my-api',
sourceAction: 'user_invited',
correlationId: req.id,
},
}).catch((err) => logger.warn({ err }, 'Invite email failed'))
In sandbox and local dev, RELAY_MODE=sandbox is set automatically. Relay logs messages instead of sending them — no real emails or SMS are delivered. You never set this manually.
// ❌ WRONG: deprecated 'from' field
options: { from: 'My Service' }
// ✅ CORRECT
options: { fromName: 'My Service' }
// ❌ WRONG: legacy JWT auth
new RelayClient({ jwtSecret: process.env.JWT_SECRET, programId: '...' })
// ✅ CORRECT
RelayClient.fromEnv()
// ❌ WRONG: await in request handler
app.post('/api/send', async (req, res) => {
await relay.sendEmail({ ... }) // blocks until delivery
res.json({ ok: true })
})
// ✅ CORRECT
app.post('/api/send', async (req, res) => {
relay.sendEmail({ ... }).catch((err) => logger.warn({ err }, 'Failed'))
res.json({ ok: true }) // responds immediately
})
fromName sets the display name only — the actual sender is always your org's domain or [email protected]retries: 0 if duplicates are unacceptable (e.g. payment receipts){{variable}} Handlebars-style interpolation+15551234567 (country code required, no spaces or dashes)Last updated: February 28, 2026