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 registrymailerRegistry.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=megamailerMoving 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.
- Copy both the
package.jsonandtsconfig.jsonfiles from theresendpackage to the new package and rename the package to@kit/megamailer - Copy the
megamailer.tsfile to the new package - Update the
index.tsfile 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 registrymailerRegistry.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
sendEmailmethod must return a Promise. Even if your SDK is callback-based, wrap it innew Promise(). - Forgetting to add to registry: Creating the mailer class isn't enough - you must call
mailerRegistry.register()orgetMailer()won't find it. - Wrong import path after refactor: If you move to a separate package, update the dynamic import in
registry.tsto match the new package name. - Edge runtime compatibility: If targeting edge (Cloudflare Workers), don't use Node.js-only APIs like
netorfs. Test withNEXT_RUNTIME=edge. - Missing pnpm workspace link: When creating a new package, run
pnpm i "@kit/yourmailer:workspace:*" --filter "@kit/mailers"to link it.