Creating a Custom Mailer in Makerkit

Learn how to create a custom mailer in Makerkit, so you can send emails using your own email provider.

Create a custom mailer by extending the Mailer abstract class and implementing sendEmail(data). Register it in packages/mailers/core/src/registry.ts with mailerRegistry.register('yourmailer', ...), then set MAILER_PROVIDER=yourmailer. The mailer receives a Zod-validated object with to, from, subject, and text/html fields.

This guide is part of the Email Configuration documentation.

A custom mailer is a provider-specific implementation of the Mailer abstract class that integrates any email service (SendGrid, Postmark, Amazon SES, etc.) into the Makerkit Prisma stack's unified email API.

  • Create a custom mailer when: your email provider isn't Nodemailer-compatible SMTP or Resend.
  • Use Nodemailer instead when: your provider offers SMTP (most do).
  • Use Resend instead when: you need edge runtime support without custom code.
  • If unsure: try Nodemailer with SMTP first - custom mailers are rarely needed.

Steps to create a custom mailer

Learn how to create a custom mailer in MakerKit.

MakerKit implements both Nodemailer (any SMTP provider) and Resend (HTTP API for edge runtimes). For other providers or custom integrations, create a custom mailer using the packages/mailers package.

The mailer class is intentionally minimal:

export abstract class Mailer<Res = unknown> {
abstract sendEmail(data: z.input<typeof MailerSchema>): Promise<Res>;
}

Create your own mailer by extending the Mailer class and implementing the sendEmail method.

Implementing the sendEmail method

The sendEmail method takes a z.input<typeof MailerSchema> object, which is Zod-validated email data containing to, from, subject, and text/html fields.

Here's an example of how to implement the sendEmail method. For simplicity, we will call our custom mailer megamailer in the following examples.

packages/mailers/core/src/megamailer.ts

import { z } from 'zod';
import { MailerSchema } from '../../shared/src/schema/mailer.schema';
export function createMegaMailer() {
return new MegaMailer();
}
class MegaMailer implements Mailer<unknown> {
async sendEmail(data: z.output<typeof MailerSchema>) {
// Implement your email sending logic here
// For example, you can use Nodemailer to send emails
// or use a third-party email provider like SendGrid
return {};
}
}

NB: for simplicity, we've placed the megamailer.ts file in the core package. However, if you have 5 minutes, you can move it to a separate package and import it in the index.ts file, just like we do for Nodemailer and Resend.

Registering the Mailer

Now that we have our custom mailer, we need to register it in the packages/mailers/core/src/registry.ts file:

packages/mailers/core/src/registry.ts

import { Mailer } from '@kit/mailers-shared';
import { createRegistry } from '@kit/shared/registry';
import { MailerProvider } from './provider-enum';
const mailerRegistry = createRegistry<Mailer, MailerProvider>();
// here we add our custom mailer to the registry
mailerRegistry.register('megamailer', async () => {
const { createMegaMailer } = await import('@kit/megamailer');
return createMegaMailer();
});
mailerRegistry.register('nodemailer', async () => {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { createNodemailerService } = await import('@kit/nodemailer');
return createNodemailerService();
} else {
throw new Error(
'Nodemailer is not available on the edge runtime. Please use another mailer.',
);
}
});
mailerRegistry.register('resend', async () => {
const { createResendMailer } = await import('@kit/resend');
return createResendMailer();
});
export { mailerRegistry };

Setting a new Mailer Provider

By setting the MAILER_PROVIDER environment variable to megamailer, you can use the Megamailer mailer in your application.

MAILER_PROVIDER=megamailer

Moving Megamailer to a separate package

Let's move the Megamailer to a separate package. To do so, we create a new package called @kit/megamailer and move the megamailer.ts file to the new package.

Let's create a package at packages/mailers/megamailer.

  1. Copy both the package.json and tsconfig.json files from the resend package to the new package and rename the package to @kit/megamailer
  2. Copy the megamailer.ts file to the new package
  3. Update the index.ts file to export the new package and use the Megamailer

Install the Megamailer package

To install the new package, run the following command:

pnpm i "@kit/megamailer:workspace:*" --filter "@kit/mailers"

Update the Mailer Registry

Now that we have the new package, we need to update the registry.ts file to export the new package and use the Megamailer.

packages/mailers/core/src/registry.ts

// here we add our custom mailer to the registry
mailerRegistry.register('megamailer', async () => {
const { createMegaMailer } = await import('@kit/megamailer');
return createMegaMailer();
});

When we added Resend support to MakerKit, the registry pattern made it a single-file change - no modifications to existing code. Custom mailers follow the same pattern.

Common Pitfalls

  • Missing Mailer implementation: The class must implement Mailer<unknown>. Forgetting this causes TypeScript errors when registering.
  • Synchronous sendEmail: The sendEmail method must return a Promise. Even if your SDK is callback-based, wrap it in new Promise().
  • Forgetting to add to registry: Creating the mailer class isn't enough - you must call mailerRegistry.register() or getMailer() won't find it.
  • Wrong import path after refactor: If you move to a separate package, update the dynamic import in registry.ts to match the new package name.
  • Edge runtime compatibility: If targeting edge (Cloudflare Workers), don't use Node.js-only APIs like net or fs. Test with NEXT_RUNTIME=edge.
  • Missing pnpm workspace link: When creating a new package, run pnpm i "@kit/yourmailer:workspace:*" --filter "@kit/mailers" to link it.

Frequently Asked Questions

Do I need a custom mailer for SendGrid?
No. SendGrid offers SMTP, so use Nodemailer with SendGrid's SMTP credentials. Custom mailers are only needed for providers without SMTP support.
Can I add custom fields like replyTo or CC?
The base MailerSchema supports to, from, subject, text, and html. Extend the schema in mailers-shared and update your implementation for additional fields.
How do I handle rate limiting?
Implement retry logic in your sendEmail method. Use exponential backoff and respect the provider's rate limit headers. Consider a queue for high-volume sending.
Can I have multiple custom mailers?
Yes. Register each with a unique name in the registry. Switch between them by changing MAILER_PROVIDER or by directly calling mailerRegistry.get('mailer-name').
How do I test my custom mailer?
Create a test script that calls your mailer directly. For integration tests, use Mailpit locally and check the captured emails. Mock the provider SDK for unit tests.

Next: Local Development (Mailpit) →