Creating a Custom Mailer in the Next.js Supabase Starter Kit

Learn how to create a custom mailer in the Next.js Supabase Starter Kit, so you can send emails using your own email provider.

Makerkit implements both Nodemailer (which can potentially support any SMTP provider) and Resend, using its HTTP API, which allows Makerkit to be compatible with edge runtimes (such as Cloudflare Workers).

However, if you want to use your own email provider, you can create a custom mailer using the packages/mailers package.

The mailer class is very simple:

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

As such, you can create your own mailer by extending the Mailer class and implementing the sendEmail method.

Implementing the sendEmail method

The sendEmail method takes in a z.infer<typeof MailerSchema> object, which is a Zod schema that defines the shape of the email data. This allows you to validate the email data before sending it.

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

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

packages/mailers/core/src/index.ts
import { z } from 'zod';
const MAILER_PROVIDER = z
.enum(['nodemailer', 'resend', 'megamailer'])
.default('nodemailer')
.parse(process.env.MAILER_PROVIDER);
/**
* @description Get the mailer based on the environment variable.
*/
export async function getMailer() {
switch (MAILER_PROVIDER) {
case 'nodemailer':
return getNodemailer();
case 'resend': {
const { createResendMailer } = await import('@kit/resend');
return createResendMailer();
}
case 'megamailer': {
const { createMegamailer } = await import('./megamailer');
return createMegamailer();
}
default:
throw new Error(`Invalid mailer: ${MAILER_PROVIDER as string}`);
}
}
async function getNodemailer() {
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.',
);
}
}

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 index.ts file

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

packages/mailers/core/src/index.ts
import { z } from 'zod';
const MAILER_PROVIDER = z
.enum(['nodemailer', 'resend', 'megamailer'])
.default('nodemailer')
.parse(process.env.MAILER_PROVIDER);
/**
* @description Get the mailer based on the environment variable.
*/
export async function getMailer() {
switch (MAILER_PROVIDER) {
case 'nodemailer':
return getNodemailer();
case 'resend': {
const { createResendMailer } = await import('@kit/resend');
return createResendMailer();
}
case 'megamailer': {
const { createMegamailer } = await import('@kit/megamailer');
return createMegamailer();
}
default:
throw new Error(`Invalid mailer: ${MAILER_PROVIDER as string}`);
}
}
async function getNodemailer() {
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.',
);
}
}