Sending Emails in the Tanstack Start Supabase SaaS Starter Kit

Send transactional emails from your Tanstack Start Supabase application using the MakerKit mailer API. Learn the email schema, error handling, and best practices.

The @kit/mailers package provides a simple, provider-agnostic API for sending emails. Use it in server functions, server routes, or any server-side code to send transactional emails.

Basic Usage

Import getMailer and call sendEmail with your email data:

import { getMailer } from '@kit/mailers';
async function sendWelcomeEmail(userEmail: string) {
const mailer = await getMailer();
await mailer.sendEmail({
to: userEmail,
from: process.env.EMAIL_SENDER!,
subject: 'Welcome to our platform',
text: 'Thanks for signing up! We are excited to have you.',
});
}

The getMailer function returns the configured mailer instance (Nodemailer or Resend) based on your MAILER_PROVIDER environment variable.

Email Schema

The sendEmail method accepts an object validated by this Zod schema:

// Simplified representation of the schema
type EmailData = {
to: string; // Recipient email (must be valid email format)
from: string; // Sender (e.g., "App Name <noreply@app.com>")
subject: string; // Email subject line
} & (
| { text: string } // Plain text body
| { html: string } // HTML body
);

You must provide exactly one of text or html. This is a discriminated union, not optional fields. Providing both properties or neither will cause a validation error at runtime.

Sending HTML Emails

For rich email content, use the html property:

import { getMailer } from '@kit/mailers';
async function sendHtmlEmail(to: string) {
const mailer = await getMailer();
await mailer.sendEmail({
to,
from: process.env.EMAIL_SENDER!,
subject: 'Your weekly summary',
html: `
<h1>Weekly Summary</h1>
<p>Here's what happened this week:</p>
<ul>
<li>5 new team members joined</li>
<li>12 tasks completed</li>
</ul>
`,
});
}

For complex HTML emails, use React Email templates instead of inline HTML strings.

Using Email Templates

MakerKit includes pre-built email templates in the @kit/email-templates package. These templates use React Email and support internationalization:

import { getMailer } from '@kit/mailers';
import { renderInviteEmail } from '@kit/email-templates';
async function sendTeamInvitation(params: {
invitedEmail: string;
teamName: string;
inviterName: string;
inviteLink: string;
}) {
const mailer = await getMailer();
// Render the React Email template to HTML
const { html, subject } = await renderInviteEmail({
teamName: params.teamName,
inviter: params.inviterName,
invitedUserEmail: params.invitedEmail,
link: params.inviteLink,
productName: 'Your App Name',
});
await mailer.sendEmail({
to: params.invitedEmail,
from: process.env.EMAIL_SENDER!,
subject,
html,
});
}

See the Email Templates guide for creating custom templates.

Error Handling

The sendEmail method returns a Promise that rejects on failure. Always wrap email sending in try-catch:

import { getMailer } from '@kit/mailers';
async function sendEmailSafely(to: string, subject: string, text: string) {
try {
const mailer = await getMailer();
await mailer.sendEmail({
to,
from: process.env.EMAIL_SENDER!,
subject,
text,
});
return { success: true };
} catch (error) {
console.error('Failed to send email:', error);
// Log to your error tracking service
// Sentry.captureException(error);
return { success: false, error: 'Failed to send email' };
}
}

Common Error Causes

ErrorCauseSolution
Validation errorInvalid email format or missing fieldsCheck to is a valid email, ensure text or html is provided
Authentication failedWrong SMTP credentialsVerify EMAIL_USER and EMAIL_PASSWORD
Connection refusedSMTP server unreachableCheck EMAIL_HOST and EMAIL_PORT, verify network access
Rate limitedToo many emails sentImplement rate limiting, use a queue for bulk sends

Using in Server Functions

Email sending works in Tanstack Start server functions:

lib/server/send-notification.functions.ts

import { createServerFn } from '@tanstack/react-start';
import { getMailer } from '@kit/mailers';
import { SendNotificationSchema } from './send-notification.schema';
export const sendNotificationFunction = createServerFn({ method: 'POST' })
.validator(SendNotificationSchema)
.handler(async ({ data }) => {
const mailer = await getMailer();
await mailer.sendEmail({
to: data.email,
from: process.env.EMAIL_SENDER!,
subject: 'New notification',
text: data.message,
});
return { success: true };
});

Using in Server Routes

For webhook handlers or external integrations:

apps/web/src/routes/api/webhooks/send-email.ts

import { createFileRoute } from '@tanstack/react-router';
import { getMailer } from '@kit/mailers';
export const Route = createFileRoute('/api/webhooks/send-email')({
server: {
handlers: {
POST: async ({ request }) => {
const { to, subject, message } = await request.json();
try {
const mailer = await getMailer();
await mailer.sendEmail({
to,
from: process.env.EMAIL_SENDER!,
subject,
text: message,
});
return Response.json({ success: true });
} catch (error) {
return Response.json(
{ error: 'Failed to send email' },
{ status: 500 },
);
}
},
},
},
});

Best Practices

Use Environment Variables for Sender

Never hardcode the sender email:

// Good
from: process.env.EMAIL_SENDER!
// Bad
from: 'noreply@example.com'

Validate Recipient Emails

Before sending, validate that the recipient email exists in your system:

import { getMailer } from '@kit/mailers';
async function sendToUser(userId: string, subject: string, text: string) {
// Fetch user from database first
const user = await getUserById(userId);
if (!user?.email) {
throw new Error('User has no email address');
}
const mailer = await getMailer();
await mailer.sendEmail({
to: user.email,
from: process.env.EMAIL_SENDER!,
subject,
text,
});
}

Queue Bulk Emails

For sending many emails, use a background job queue to avoid timeouts and handle retries:

// Instead of this:
for (const user of users) {
await sendEmail(user.email); // Slow, no retry handling
}
// Use a queue like Trigger.dev, Inngest, or BullMQ
await emailQueue.addBulk(
users.map(user => ({
name: 'send-email',
data: { email: user.email, template: 'weekly-digest' },
}))
);

Next Steps