BunShip uses React Email for templating and Resend for delivery. Templates live in the packages/emails/ package and render to HTML that works across all major email clients.
Email Package Overview
packages/emails/
├── src/
│ ├── components/
│ │ └── Layout.tsx # Shared header, footer, and styles
│ ├── templates/
│ │ ├── welcome.tsx # Post-registration welcome
│ │ ├── verify-email.tsx # Email address verification
│ │ ├── reset-password.tsx # Password reset link
│ │ ├── team-invite.tsx # Organization invitation
│ │ ├── invoice.tsx # Payment receipt
│ │ └── subscription-canceled.tsx # Cancellation confirmation
│ └── index.ts # Public exports
├── package.json
└── tsconfig.json
Emails are sent through a BullMQ background job queue, so sending never blocks API responses.
Available Templates
BunShip ships with six templates that cover the core SaaS transactional email flows.
Welcome Email
Sent after a user registers. Includes a link to the dashboard.
import { WelcomeEmail } from "@bunship/emails" ;
< WelcomeEmail
name = "John Doe"
dashboardUrl = "https://app.yourdomain.com/dashboard"
/>
Verify Email
Sent to confirm an email address. Includes a verification link and a resend option.
import { VerifyEmail } from "@bunship/emails" ;
< VerifyEmail
name = "John Doe"
verificationUrl = "https://app.yourdomain.com/verify?token=abc123"
resendUrl = "https://app.yourdomain.com/resend-verification"
/>
Reset Password
Sent when a user requests a password reset.
import { ResetPasswordEmail } from "@bunship/emails" ;
< ResetPasswordEmail
name = "John Doe"
resetUrl = "https://app.yourdomain.com/reset-password?token=xyz789"
/>
Team Invite
Sent when a team member is invited to join an organization.
import { TeamInviteEmail } from "@bunship/emails" ;
< TeamInviteEmail
inviteeName = "Jane Smith"
inviterName = "John Doe"
organizationName = "Acme Corp"
role = "developer"
inviteUrl = "https://app.yourdomain.com/accept-invite?token=inv123"
/>
Invoice
Sent after a successful payment.
import { InvoiceEmail } from "@bunship/emails" ;
< InvoiceEmail
name = "John Doe"
invoiceNumber = "INV-2024-001"
amount = "49.00"
currency = "USD"
planName = "Pro Plan"
billingPeriod = "Jan 1 - Jan 31, 2024"
paymentDate = "January 1, 2024"
invoiceUrl = "https://app.yourdomain.com/invoices/inv-001"
portalUrl = "https://app.yourdomain.com/billing"
/>
Subscription Canceled
Sent when a user cancels their subscription.
import { SubscriptionCanceledEmail } from "@bunship/emails" ;
< SubscriptionCanceledEmail
name = "John Doe"
planName = "Pro Plan"
accessEndDate = "January 31, 2024"
resubscribeUrl = "https://app.yourdomain.com/resubscribe"
feedbackUrl = "https://app.yourdomain.com/feedback"
/>
Template Structure
Every template follows the same pattern: a props interface, a React component, and inline styles.
// packages/emails/src/templates/welcome.tsx
import {
Body ,
Container ,
Head ,
Heading ,
Html ,
Link ,
Preview ,
Text ,
} from "@react-email/components" ;
import { appConfig } from "@bunship/config" ;
interface WelcomeEmailProps {
name : string ;
dashboardUrl : string ;
}
export function WelcomeEmail ({ name , dashboardUrl } : WelcomeEmailProps ) {
return (
< Html >
< Head />
< Preview > Welcome to { appConfig . name } </ Preview >
< Body style = { main } >
< Container style = { container } >
< Heading style = { heading } >
Welcome , { name } !
</ Heading >
< Text style = { text } >
Thanks for signing up . Your account is ready .
</ Text >
< Link href = { dashboardUrl } style = { button } >
Go to Dashboard
</ Link >
</ Container >
</ Body >
</ Html >
);
}
The Layout Component
All built-in templates use a shared Layout component that provides consistent branding, a header with your logo, and a footer with your company details.
import { Layout } from "../components/Layout" ;
export function MyEmail ({ name } : Props ) {
return (
< Layout previewText = "Preview text for inbox snippets" >
{ /* Your email content goes here */ }
</ Layout >
);
}
Edit packages/emails/src/components/Layout.tsx to change:
Brand logo URL and dimensions
Header background color
Footer text and links
Default font family
Creating a Custom Template
Create the template file
Add a new file in packages/emails/src/templates/: // packages/emails/src/templates/usage-alert.tsx
import { Text , Section , Button } from "@react-email/components" ;
import { Layout } from "../components/Layout" ;
import { appConfig } from "@bunship/config" ;
export interface UsageAlertEmailProps {
name : string ;
resourceName : string ;
currentUsage : number ;
limit : number ;
upgradeUrl : string ;
}
export function UsageAlertEmail ({
name ,
resourceName ,
currentUsage ,
limit ,
upgradeUrl ,
} : UsageAlertEmailProps ) {
const percentage = Math . round (( currentUsage / limit ) * 100 );
return (
< Layout previewText = { ` ${ resourceName } usage at ${ percentage } %` } >
< Section >
< Text style = { heading } > Usage Alert </ Text >
< Text style = { paragraph } >
Hi { name },
</ Text >
< Text style = { paragraph } >
Your { resourceName } usage has reached { " " }
< strong >{ percentage } %</ strong > of your plan limit
({currentUsage.toLocaleString()} of { " " }
{limit.toLocaleString()}).
</ Text >
< Text style = { paragraph } >
To avoid service interruptions , consider upgrading
your plan .
</ Text >
< Button style = { button } href = { upgradeUrl } >
Upgrade Plan
</ Button >
< Text style = { footer } >
You are receiving this because usage alerts are
enabled for your { appConfig . name } account .
</ Text >
</ Section >
</ Layout >
);
}
const heading = {
fontSize: "24px" ,
fontWeight: "bold" as const ,
margin: "0 0 24px" ,
};
const paragraph = {
fontSize: "16px" ,
lineHeight: "26px" ,
margin: "0 0 16px" ,
};
const button = {
backgroundColor: "#5046e5" ,
borderRadius: "6px" ,
color: "#fff" ,
display: "block" as const ,
fontSize: "16px" ,
fontWeight: "bold" as const ,
textAlign: "center" as const ,
textDecoration: "none" ,
padding: "12px 24px" ,
margin: "24px 0" ,
};
const footer = {
fontSize: "14px" ,
color: "#666" ,
marginTop: "32px" ,
};
Export from the package
Add the export to packages/emails/src/index.ts: export { UsageAlertEmail } from "./templates/usage-alert" ;
export type { UsageAlertEmailProps } from "./templates/usage-alert" ;
Send the email from your API
Use the email queue to send asynchronously: import { renderEmail , UsageAlertEmail } from "@bunship/emails" ;
const html = await renderEmail (
UsageAlertEmail ({
name: user . fullName ?? "there" ,
resourceName: "API requests" ,
currentUsage: 8500 ,
limit: 10000 ,
upgradeUrl: ` ${ appConfig . frontendUrl } /billing/upgrade` ,
})
);
await emailQueue . add ( "send" , {
to: user . email ,
subject: "Usage alert: API requests at 85%" ,
html ,
});
Rendering Emails
The @bunship/emails package exports two render functions:
HTML Rendering
import { renderEmail , WelcomeEmail } from "@bunship/emails" ;
const html = await renderEmail (
WelcomeEmail ({
name: "John Doe" ,
dashboardUrl: "https://app.yourdomain.com/dashboard" ,
})
);
Plain Text Rendering
For email clients that do not support HTML:
import { renderEmailText , WelcomeEmail } from "@bunship/emails" ;
const text = await renderEmailText (
WelcomeEmail ({
name: "John Doe" ,
dashboardUrl: "https://app.yourdomain.com/dashboard" ,
})
);
Sending with Resend
BunShip sends emails through Resend via the background job queue. The queue is configured in featuresConfig.jobs.queues.email:
// Direct send (not recommended for API routes)
import { Resend } from "resend" ;
const resend = new Resend ( process . env . RESEND_API_KEY );
await resend . emails . send ({
from: ` ${ appConfig . name } <[email protected] >` ,
to: "[email protected] " ,
subject: "Welcome!" ,
html ,
});
// Queue-based send (recommended)
await emailQueue . add ( "send" , {
to: user . email ,
subject: "Welcome to " + appConfig . name ,
html ,
});
Always use the queue for sending emails from API route handlers. This keeps response times fast
and handles retries automatically if the email provider is temporarily unavailable.
Testing Emails Locally
React Email includes a development server that renders your templates in the browser with hot reload.
cd packages/emails
bun run dev
This starts a preview server at http://localhost:3000 where you can:
Browse all templates in a sidebar
Preview with sample data
Toggle between desktop and mobile widths
View the raw HTML output
Sending Test Emails
To send a real test email through Resend (requires a valid API key):
import { renderEmail , WelcomeEmail } from "@bunship/emails" ;
import { Resend } from "resend" ;
const resend = new Resend ( process . env . RESEND_API_KEY );
const html = await renderEmail (
WelcomeEmail ({
name: "Test User" ,
dashboardUrl: "http://localhost:5173/dashboard" ,
})
);
await resend . emails . send ({
from: "[email protected] " ,
to: "[email protected] " ,
subject: "[TEST] Welcome Email" ,
html ,
});
Styling Guidelines
Email clients have limited and inconsistent CSS support. Follow these rules for reliable rendering:
Email clients strip <style> tags and ignore external stylesheets. Define styles as JavaScript objects and apply them via the style prop. const heading = {
fontSize: "24px" ,
fontWeight: "bold" as const ,
margin: "0 0 24px" ,
};
< Text style = { heading } > Hello </ Text >
Use system font stacks that work everywhere: fontFamily : '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' ,
Avoid complex CSS layouts (flexbox, grid). Use React Email’s <Section>, <Row>, and <Column> components for multi-column layouts.
The built-in templates are tested across Gmail, Outlook, Apple Mail, Yahoo Mail, and Thunderbird. Test your custom templates in the same clients before deploying.
Next Steps