Creating a Custom Mailer
Integrate third-party email providers like SendGrid, Postmark, or AWS SES into your Next.js Supabase application by creating a custom mailer implementation.
MakerKit's mailer system is designed to be extensible. While Nodemailer and Resend cover most use cases, you may need to integrate a different email provider like SendGrid, Postmark, Mailchimp Transactional, or AWS SES.
This guide shows you how to create a custom mailer that plugs into MakerKit's email system.
Mailer Architecture
The mailer system uses a registry pattern with lazy loading:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐│ Application │────▶│ Mailer Registry │────▶│ Your Provider ││ getMailer() │ │ (lazy loading) │ │ (SendGrid etc) │└─────────────────┘ └──────────────────┘ └─────────────────┘- Your code calls
getMailer() - The registry checks
MAILER_PROVIDERenvironment variable - The matching mailer implementation is loaded and returned
- You call
sendEmail()on the returned instance
Creating a Custom Mailer
Let's create a mailer for SendGrid as an example. The same pattern works for any provider.
Step 1: Implement the Mailer Class
Create a new file in the mailers package:
packages/mailers/sendgrid/src/index.ts
import 'server-only';import { z } from 'zod';import { Mailer, MailerSchema } from '@kit/mailers-shared';type Config = z.infer<typeof MailerSchema>;const SENDGRID_API_KEY = z .string({ description: 'SendGrid API key', required_error: 'SENDGRID_API_KEY environment variable is required', }) .parse(process.env.SENDGRID_API_KEY);export function createSendGridMailer() { return new SendGridMailer();}class SendGridMailer implements Mailer { async sendEmail(config: Config) { const body = { personalizations: [ { to: [{ email: config.to }], }, ], from: { email: config.from }, subject: config.subject, content: [ { type: 'text' in config ? 'text/plain' : 'text/html', value: 'text' in config ? config.text : config.html, }, ], }; const response = await fetch('https://api.sendgrid.com/v3/mail/send', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${SENDGRID_API_KEY}`, }, body: JSON.stringify(body), }); if (!response.ok) { const error = await response.text(); throw new Error(`SendGrid error: ${response.status} - ${error}`); } return { success: true }; }}Step 2: Create Package Structure
If creating a separate package (recommended), set up the structure:
packages/mailers/sendgrid/├── src/│ └── index.ts├── package.json└── tsconfig.jsonpackage.json:
packages/mailers/sendgrid/package.json
{ "name": "@kit/sendgrid", "version": "0.0.1", "private": true, "main": "./src/index.ts", "types": "./src/index.ts", "dependencies": { "@kit/mailers-shared": "workspace:*", "server-only": "^0.0.1", "zod": "^3.23.0" }}tsconfig.json:
packages/mailers/sendgrid/tsconfig.json
{ "extends": "@kit/tsconfig/base.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src/**/*"]}Step 3: Install the Package
Add the new package as a dependency to the mailers core package:
pnpm i "@kit/sendgrid:workspace:*" --filter "@kit/mailers"Step 4: Register the Mailer
Add your mailer to the registry:
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>();// Existing mailersmailerRegistry.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();});// Add your custom mailermailerRegistry.register('sendgrid', async () => { const { createSendGridMailer } = await import('@kit/sendgrid'); return createSendGridMailer();});export { mailerRegistry };Step 5: Update Provider Enum
Add your provider to the providers array:
packages/mailers/core/src/provider-enum.ts
import { z } from 'zod';const MAILER_PROVIDERS = [ 'nodemailer', 'resend', 'sendgrid', // Add this] as const;const MAILER_PROVIDER = z .enum(MAILER_PROVIDERS) .default('nodemailer') .parse(process.env.MAILER_PROVIDER);export { MAILER_PROVIDER };export type MailerProvider = (typeof MAILER_PROVIDERS)[number];Step 6: Configure Environment Variables
Set the environment variable to use your mailer:
MAILER_PROVIDER=sendgridSENDGRID_API_KEY=SG.your-api-key-hereEMAIL_SENDER=YourApp <noreply@yourdomain.com>Edge Runtime Compatibility
If your mailer uses HTTP APIs (not SMTP), it can run on edge runtimes. The key requirements:
- No Node.js-specific APIs: Avoid
fs,net,crypto(use Web Crypto instead) - Use fetch: HTTP requests via
fetchwork everywhere - Import server-only: Add
import 'server-only'to prevent client-side usage
Checking Runtime Compatibility
mailerRegistry.register('my-mailer', async () => { // This check is optional but recommended for documentation if (process.env.NEXT_RUNTIME === 'edge') { // Edge-compatible path const { createMyMailer } = await import('@kit/my-mailer'); return createMyMailer(); } else { // Node.js path (can use SMTP, etc.) const { createMyMailer } = await import('@kit/my-mailer'); return createMyMailer(); }});For Nodemailer (SMTP-based), edge runtime is not supported. For HTTP-based providers like Resend, SendGrid, or Postmark, edge runtime works fine.
Example Implementations
Postmark
import 'server-only';import { z } from 'zod';import { Mailer, MailerSchema } from '@kit/mailers-shared';const POSTMARK_API_KEY = z.string().parse(process.env.POSTMARK_API_KEY);export function createPostmarkMailer() { return new PostmarkMailer();}class PostmarkMailer implements Mailer { async sendEmail(config: z.infer<typeof MailerSchema>) { const body = { From: config.from, To: config.to, Subject: config.subject, ...'text' in config ? { TextBody: config.text } : { HtmlBody: config.html }, }; const response = await fetch('https://api.postmarkapp.com/email', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Postmark-Server-Token': POSTMARK_API_KEY, }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`Postmark error: ${response.statusText}`); } return response.json(); }}AWS SES (HTTP API)
import 'server-only';import { z } from 'zod';import { Mailer, MailerSchema } from '@kit/mailers-shared';// Using AWS SDK v3 (modular)import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';const sesClient = new SESClient({ region: process.env.AWS_REGION ?? 'us-east-1', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, },});export function createSESMailer() { return new SESMailer();}class SESMailer implements Mailer { async sendEmail(config: z.infer<typeof MailerSchema>) { const command = new SendEmailCommand({ Source: config.from, Destination: { ToAddresses: [config.to], }, Message: { Subject: { Data: config.subject }, Body: 'text' in config ? { Text: { Data: config.text } } : { Html: { Data: config.html } }, }, }); return sesClient.send(command); }}Testing Your Custom Mailer
Test your mailer in isolation before integrating:
// test/sendgrid-mailer.test.tsimport { createSendGridMailer } from '@kit/sendgrid';describe('SendGrid Mailer', () => { it('sends an email', async () => { const mailer = createSendGridMailer(); // Use a test email or mock the API await mailer.sendEmail({ to: 'test@example.com', from: 'noreply@yourdomain.com', subject: 'Test Email', text: 'This is a test email', }); });});Quick Integration (Without Separate Package)
For a faster setup without creating a separate package, add your mailer directly to the core package:
packages/mailers/core/src/sendgrid.ts
import 'server-only';import { z } from 'zod';import { Mailer, MailerSchema } from '@kit/mailers-shared';// ... implementationThen register it:
packages/mailers/core/src/registry.ts
mailerRegistry.register('sendgrid', async () => { const { createSendGridMailer } = await import('./sendgrid'); return createSendGridMailer();});This approach is faster but puts all mailer code in one package. Use separate packages for cleaner separation.
Next Steps
- Configure your email provider with environment variables
- Create email templates with React Email
- Test emails locally with Mailpit