# 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.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/emails/custom-mailer*

---

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](./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 repo'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.

{% sequence title="Steps to create a custom mailer" description="Learn how to create a custom mailer in MakerKit." %}

[Implementing the sendEmail method](#implementing-the-sendemail-method)

[Registering the Mailer](#registering-the-mailer)

[Setting a new Mailer Provider](#setting-a-new-mailer-provider)

[Moving Megamailer to a separate package](#moving-megamailer-to-a-separate-package)

{% /sequence %}

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:

```tsx
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.

```tsx {% title="packages/mailers/core/src/megamailer.ts" %}
import { z } from 'zod';

import { Mailer, MailerSchema } from '@kit/mailers-shared';

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:

```tsx {% title="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.

```tsx {% title="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.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Do I need a custom mailer for SendGrid?", "answer": "No. SendGrid offers SMTP, so use Nodemailer with SendGrid's SMTP credentials. Custom mailers are only needed for providers without SMTP support."},
     {"question": "Can I add custom fields like replyTo or CC?", "answer": "The base MailerSchema supports to, from, subject, text, and html. Extend the schema in mailers-shared and update your implementation for additional fields."},
     {"question": "How do I handle rate limiting?", "answer": "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."},
     {"question": "Can I have multiple custom mailers?", "answer": "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')."},
     {"question": "How do I test my custom mailer?", "answer": "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) →](./mailpit)
