Next.js Supabase V3 (#463)

Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
This commit is contained in:
Giancarlo Buomprisco
2026-03-24 13:40:38 +08:00
committed by GitHub
parent 4912e402a3
commit 7ebff31475
840 changed files with 71395 additions and 20095 deletions

View File

@@ -0,0 +1,242 @@
---
status: "published"
title: "Supabase Authentication Email Templates"
label: "Authentication Emails"
description: "Configure Supabase Auth email templates for email verification, password reset, and magic links. Learn about PKCE flow and token hash strategy for cross-browser compatibility."
order: 3
---
Supabase Auth sends emails for authentication flows like email verification, password reset, and magic links. MakerKit provides custom templates that use the token hash strategy, which is required for PKCE (Proof Key for Code Exchange) authentication to work correctly across different browsers and devices.
## Why Custom Templates Matter
Supabase's default email templates use a redirect-based flow that can break when users open email links in a different browser than where they started authentication. This happens because:
1. User starts sign-up in Chrome
2. Clicks verification link in their email client (which may open in Safari)
3. Safari doesn't have the PKCE code verifier stored
4. Authentication fails
MakerKit's templates solve this by using the **token hash strategy**, which passes the verification token directly to a server-side endpoint that exchanges it for a session.
{% alert type="warning" title="Required for Production" %}
You must replace Supabase's default email templates with MakerKit's templates. Without this change, users may experience authentication failures when clicking email links from different browsers or email clients.
{% /alert %}
## Template Types
Supabase Auth uses six email templates:
| Template | Trigger | Purpose |
|----------|---------|---------|
| Confirm signup | New user registration | Verify email address |
| Magic link | Passwordless login | One-click sign in |
| Change email | Email update request | Verify new email |
| Reset password | Password reset request | Secure password change |
| Invite user | Admin invites user | Join the platform |
| OTP | Code-based verification | Numeric verification code |
## Template Location
MakerKit's pre-built templates are in your project:
```
apps/web/supabase/templates/
├── change-email-address.html
├── confirm-email.html
├── invite-user.html
├── magic-link.html
├── otp.html
└── reset-password.html
```
## How the Token Hash Strategy Works
The templates use this URL format:
```
{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&callback={{ .RedirectTo }}
```
This flow:
1. Supabase generates a `token_hash` for the email action
2. User clicks the link in their email
3. Request goes to `/auth/confirm` (a server-side route)
4. Server exchanges the token hash for a session using Supabase Admin API
5. User is authenticated and redirected to the callback URL
This works regardless of which browser opens the link because the token hash is self-contained.
## Configuring Templates in Supabase
### Option 1: Using Supabase Dashboard
1. Go to your Supabase project dashboard
2. Navigate to **Authentication** → **Email Templates**
3. For each template type, replace the content with the corresponding MakerKit template
4. Save changes
### Option 2: Using Supabase CLI (Recommended)
The templates in `apps/web/supabase/templates/` are automatically applied when you run migrations:
```bash
supabase db push
```
Or link and push:
```bash
supabase link --project-ref your-project-ref
supabase db push
```
## Customizing Templates
To customize the templates with your branding:
### 1. Clone the Email Starter Repository
MakerKit provides a separate repository for generating email templates:
```bash
git clone https://github.com/makerkit/makerkit-emails-starter
cd makerkit-emails-starter
pnpm install
```
### 2. Customize the Templates
The starter uses React Email. Edit the templates in `src/emails/`:
- Update colors to match your brand
- Add your logo
- Modify copy and messaging
- Adjust layout as needed
### 3. Export the Templates
Generate the HTML templates:
```bash
pnpm build
```
### 4. Replace Templates in Your Project
Copy the generated HTML files to your MakerKit project:
```bash
cp dist/*.html /path/to/your-app/apps/web/supabase/templates/
```
### 5. Push to Supabase
```bash
cd /path/to/your-app
supabase db push
```
## Template Variables
Supabase provides these variables in email templates:
| Variable | Description | Available In |
|----------|-------------|--------------|
| `{{ .SiteURL }}` | Your application URL | All templates |
| `{{ .TokenHash }}` | Verification token | All templates |
| `{{ .RedirectTo }}` | Callback URL after auth | All templates |
| `{{ .Email }}` | User's email address | All templates |
| `{{ .Token }}` | OTP code (6 digits) | OTP template only |
| `{{ .ConfirmationURL }}` | Legacy redirect URL | All templates (not recommended) |
## Example: Confirm Email Template
Here's the structure of a confirmation email template:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Confirm your email</h1>
<p>Click the button below to confirm your email address.</p>
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&callback={{ .RedirectTo }}">
Confirm Email
</a>
<p>If you didn't create an account, you can safely ignore this email.</p>
</body>
</html>
```
The key is the URL structure: `/auth/confirm?token_hash={{ .TokenHash }}&type=email&callback={{ .RedirectTo }}`
## Configuring the Auth Confirm Route
MakerKit includes the server-side route that handles token exchange at `apps/web/app/[locale]/auth/confirm/route.ts` in your project. This route:
1. Receives the token hash from the email link
2. Verifies the token with Supabase
3. Creates a session
4. Redirects the user to their destination
You don't need to modify this route unless you have custom requirements.
## Testing Authentication Emails
### Local Development
In local development, emails are captured by Mailpit:
1. Start your development server: `pnpm dev`
2. Trigger an auth action (sign up, password reset, etc.)
3. Open Mailpit at [http://localhost:54324](http://localhost:54324)
4. Find and click the verification link
### Production Testing
Before going live:
1. Create a test account with a real email address
2. Verify the email arrives and looks correct
3. Click the verification link and confirm it works
4. Test in multiple email clients (Gmail, Outlook, Apple Mail)
5. Test opening links in different browsers
## Troubleshooting
### Links Not Working
If email links fail to authenticate:
- Verify templates use `token_hash` not `ConfirmationURL`
- Check that `{{ .SiteURL }}` matches your production URL
- Ensure the `/auth/confirm` route is deployed
### Emails Not Arriving
If emails don't arrive:
- Check Supabase's email logs in the dashboard
- Verify your email provider (Supabase's built-in or custom SMTP) is configured
- Check spam folders
- For production, configure a custom SMTP provider in Supabase settings
### Wrong Redirect URL
If users are redirected incorrectly:
- Check the `callback` parameter in your auth calls
- Verify `NEXT_PUBLIC_SITE_URL` is set correctly
- Ensure redirect URLs are in Supabase's allowed list
## Next Steps
- [Configure your email provider](/docs/next-supabase-turbo/email-configuration) for transactional emails
- [Create custom email templates](/docs/next-supabase-turbo/email-templates) with React Email
- [Test emails locally](/docs/next-supabase-turbo/emails/inbucket) with Mailpit

View File

@@ -0,0 +1,377 @@
---
status: "published"
title: "Creating a Custom Mailer"
label: "Custom Mailer"
description: "Integrate third-party email providers like SendGrid, Postmark, or AWS SES into your Next.js Supabase application by creating a custom mailer implementation."
order: 5
---
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) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```
1. Your code calls `getMailer()`
2. The registry checks `MAILER_PROVIDER` environment variable
3. The matching mailer implementation is loaded and returned
4. 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:
```tsx {% title="packages/mailers/sendgrid/src/index.ts" %}
import 'server-only';
import * as 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.json
```
**package.json:**
```json {% title="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": "catalog:"
}
}
```
**tsconfig.json:**
```json {% title="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:
```bash
pnpm i "@kit/sendgrid:workspace:*" --filter "@kit/mailers"
```
### Step 4: Register the Mailer
Add your mailer to the registry:
```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>();
// Existing mailers
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();
});
// Add your custom mailer
mailerRegistry.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:
```tsx {% title="packages/mailers/core/src/provider-enum.ts" %}
import * as 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:
```bash
MAILER_PROVIDER=sendgrid
SENDGRID_API_KEY=SG.your-api-key-here
EMAIL_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:
1. **No Node.js-specific APIs**: Avoid `fs`, `net`, `crypto` (use Web Crypto instead)
2. **Use fetch**: HTTP requests via `fetch` work everywhere
3. **Import server-only**: Add `import 'server-only'` to prevent client-side usage
### Checking Runtime Compatibility
```tsx
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
```tsx
import 'server-only';
import * as 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)
```tsx
import 'server-only';
import * as 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:
```tsx
// test/sendgrid-mailer.test.ts
import { 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:
```tsx {% title="packages/mailers/core/src/sendgrid.ts" %}
import 'server-only';
import * as z from 'zod';
import { Mailer, MailerSchema } from '@kit/mailers-shared';
// ... implementation
```
Then register it:
```tsx {% title="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](/docs/next-supabase-turbo/email-configuration) with environment variables
- [Create email templates](/docs/next-supabase-turbo/email-templates) with React Email
- [Test emails locally](/docs/next-supabase-turbo/emails/inbucket) with Mailpit

View File

@@ -0,0 +1,184 @@
---
status: "published"
label: "Email Configuration"
description: "Configure Nodemailer (SMTP) or Resend to send transactional emails from your Next.js Supabase application. Learn the difference between MakerKit emails and Supabase Auth emails."
title: "Email Configuration in the Next.js Supabase SaaS Starter Kit"
order: 0
---
MakerKit uses the `@kit/mailers` package to send transactional emails like team invitations and account notifications. You can choose between Nodemailer (any SMTP provider) or Resend (HTTP API).
## MakerKit vs Supabase Auth Emails
Before configuring your email provider, understand the two email systems in your application:
| Email Type | Purpose | Configuration | Examples |
|------------|---------|---------------|----------|
| **MakerKit Emails** | Application transactional emails | Environment variables | Team invitations, account deletion, OTP codes |
| **Supabase Auth Emails** | Authentication flows | Supabase Dashboard | Email verification, password reset, magic links |
This guide covers MakerKit email configuration. For Supabase Auth emails, see the [Authentication Emails](/docs/next-supabase-turbo/authentication-emails) guide.
## Choosing a Mailer Provider
MakerKit supports two mailer implementations:
### Nodemailer (Default)
Best for: Most production deployments using any SMTP provider (SendGrid, Mailgun, Amazon SES, etc.)
- Works with any SMTP server
- Requires Node.js runtime (not compatible with Edge)
- Full control over SMTP configuration
### Resend
Best for: Edge runtime deployments, simpler setup, or if you're already using Resend
- HTTP-based API (Edge compatible)
- No SMTP configuration needed
- Requires Resend account and API key
## Configuring Nodemailer
Nodemailer is the default provider. Set these environment variables in `apps/web/.env`:
```bash
MAILER_PROVIDER=nodemailer
# SMTP Configuration
EMAIL_HOST=smtp.your-provider.com
EMAIL_PORT=587
EMAIL_USER=your-smtp-username
EMAIL_PASSWORD=your-smtp-password
EMAIL_TLS=true
EMAIL_SENDER=YourApp <hello@yourapp.com>
```
### Environment Variables Explained
| Variable | Description | Example |
|----------|-------------|---------|
| `EMAIL_HOST` | SMTP server hostname | `smtp.sendgrid.net`, `email-smtp.us-east-1.amazonaws.com` |
| `EMAIL_PORT` | SMTP port (587 for STARTTLS, 465 for SSL) | `587` |
| `EMAIL_USER` | SMTP authentication username | Varies by provider |
| `EMAIL_PASSWORD` | SMTP authentication password or API key | Varies by provider |
| `EMAIL_TLS` | Use secure connection (`true` for SSL on port 465, `false` for STARTTLS on port 587) | `true` |
| `EMAIL_SENDER` | Sender name and email address | `MyApp <noreply@myapp.com>` |
**Note**: `EMAIL_TLS` maps to Nodemailer's `secure` option. When `true`, the connection uses SSL/TLS from the start (typically port 465). When `false`, the connection starts unencrypted and upgrades via STARTTLS (typically port 587). Most modern providers use port 587 with `EMAIL_TLS=false`.
### Provider-Specific Configuration
#### SendGrid
```bash
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
EMAIL_USER=apikey
EMAIL_PASSWORD=SG.your-api-key-here
EMAIL_TLS=true
EMAIL_SENDER=YourApp <verified-sender@yourdomain.com>
```
#### Amazon SES
```bash
EMAIL_HOST=email-smtp.us-east-1.amazonaws.com
EMAIL_PORT=587
EMAIL_USER=your-ses-smtp-username
EMAIL_PASSWORD=your-ses-smtp-password
EMAIL_TLS=true
EMAIL_SENDER=YourApp <verified@yourdomain.com>
```
#### Mailgun
```bash
EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587
EMAIL_USER=postmaster@your-domain.mailgun.org
EMAIL_PASSWORD=your-mailgun-password
EMAIL_TLS=true
EMAIL_SENDER=YourApp <noreply@your-domain.mailgun.org>
```
### EMAIL_SENDER Format
The `EMAIL_SENDER` variable accepts two formats:
```bash
# With display name (recommended)
EMAIL_SENDER=YourApp <hello@yourapp.com>
# Email only
EMAIL_SENDER=hello@yourapp.com
```
{% alert type="warn" title="Verify Your Sender Domain" %}
Most email providers require you to verify your sending domain before emails will be delivered. Check your provider's documentation for domain verification instructions.
{% /alert %}
## Configuring Resend
To use Resend instead of Nodemailer:
```bash
MAILER_PROVIDER=resend
RESEND_API_KEY=re_your-api-key
EMAIL_SENDER=YourApp <hello@yourapp.com>
```
### Getting a Resend API Key
1. Create an account at [resend.com](https://resend.com)
2. Add and verify your sending domain
3. Generate an API key from the dashboard
4. Add the key to your environment variables
{% alert type="default" title="Edge Runtime Compatible" %}
Resend uses HTTP requests instead of SMTP, making it compatible with Vercel Edge Functions and Cloudflare Workers. If you deploy to edge runtimes, Resend is the recommended choice.
{% /alert %}
## Verifying Your Configuration
After configuring your email provider, test it by triggering an email in your application:
1. Start your development server: `pnpm dev`
2. Sign up a new user and invite them to a team
3. Check that the invitation email arrives
For local development, emails are captured by Mailpit at [http://localhost:54324](http://localhost:54324). See the [Local Development](/docs/next-supabase-turbo/emails/inbucket) guide for details.
## Common Configuration Errors
### Connection Timeout
If emails fail with connection timeout errors:
- Verify `EMAIL_HOST` and `EMAIL_PORT` are correct
- Check if your hosting provider blocks outbound SMTP (port 25, 465, or 587)
- Some providers like Vercel block raw SMTP; use Resend instead
### Authentication Failed
If you see authentication errors:
- Double-check `EMAIL_USER` and `EMAIL_PASSWORD`
- Some providers use API keys as passwords (e.g., SendGrid uses `apikey` as username)
- Ensure your credentials have sending permissions
### Emails Not Delivered
If emails send but don't arrive:
- Verify your sender domain is authenticated (SPF, DKIM, DMARC)
- Check the spam folder
- Review your email provider's delivery logs
- Ensure `EMAIL_SENDER` uses a verified email address
## Next Steps
- [Send your first email](/docs/next-supabase-turbo/sending-emails) using the mailer API
- [Create custom email templates](/docs/next-supabase-turbo/email-templates) with React Email
- [Configure Supabase Auth emails](/docs/next-supabase-turbo/authentication-emails) for verification flows

View File

@@ -0,0 +1,350 @@
---
status: "published"
label: "Email Templates"
description: "Create branded email templates with React Email in your Next.js Supabase application. Learn the template architecture, i18n support, and how to build custom templates."
title: "Email Templates in the Next.js Supabase SaaS Starter Kit"
order: 2
---
MakerKit uses [React Email](https://react.email) to create type-safe, responsive email templates. Templates are stored in the `@kit/email-templates` package and support internationalization out of the box.
## Template Architecture
The email templates package is organized as follows:
```
packages/email-templates/
├── src/
│ ├── components/ # Reusable email components
│ │ ├── body-style.tsx
│ │ ├── content.tsx
│ │ ├── cta-button.tsx
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ ├── heading.tsx
│ │ └── wrapper.tsx
│ ├── emails/ # Email templates
│ │ ├── account-delete.email.tsx
│ │ ├── invite.email.tsx
│ │ └── otp.email.tsx
│ ├── lib/
│ │ └── i18n.ts # i18n initialization
│ └── locales/ # Translation files
│ └── en/
│ ├── account-delete-email.json
│ ├── invite-email.json
│ └── otp-email.json
```
## Built-in Templates
MakerKit includes three email templates:
| Template | Function | Purpose |
|----------|----------|---------|
| Team Invitation | `renderInviteEmail` | Invite users to join a team |
| Account Deletion | `renderAccountDeleteEmail` | Confirm account deletion |
| OTP Code | `renderOtpEmail` | Send one-time password codes |
## Using Templates
Each template exports an async render function that returns HTML and a subject line:
```tsx
import { getMailer } from '@kit/mailers';
import { renderInviteEmail } from '@kit/email-templates';
async function sendInvitation() {
const { html, subject } = await renderInviteEmail({
teamName: 'Acme Corp',
teamLogo: 'https://example.com/logo.png', // optional
inviter: 'John Doe', // can be undefined if inviter is unknown
invitedUserEmail: 'jane@example.com',
link: 'https://app.example.com/invite/abc123',
productName: 'Your App',
language: 'en', // optional, defaults to NEXT_PUBLIC_DEFAULT_LOCALE
});
const mailer = await getMailer();
await mailer.sendEmail({
to: 'jane@example.com',
from: process.env.EMAIL_SENDER!,
subject,
html,
});
}
```
## Creating Custom Templates
To create a new email template:
### 1. Create the Template File
Create a new file in `packages/email-templates/src/emails/`:
```tsx {% title="packages/email-templates/src/emails/welcome.email.tsx" %}
import {
Body,
Head,
Html,
Preview,
Tailwind,
Text,
render,
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import { EmailContent } from '../components/content';
import { CtaButton } from '../components/cta-button';
import { EmailFooter } from '../components/footer';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
interface Props {
userName: string;
productName: string;
dashboardUrl: string;
language?: string;
}
export async function renderWelcomeEmail(props: Props) {
const namespace = 'welcome-email';
const { t } = await initializeEmailI18n({
language: props.language,
namespace,
});
const previewText = t('previewText', {
productName: props.productName,
});
const subject = t('subject', {
productName: props.productName,
});
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>
{t('heading', { productName: props.productName })}
</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t('hello', { userName: props.userName })}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t('mainText')}
</Text>
<CtaButton href={props.dashboardUrl}>
{t('ctaButton')}
</CtaButton>
</EmailContent>
<EmailFooter>{props.productName}</EmailFooter>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
};
}
```
### 2. Create Translation File
Add a translation file in `packages/email-templates/src/locales/en/`:
```json {% title="packages/email-templates/src/locales/en/welcome-email.json" %}
{
"subject": "Welcome to {productName}",
"previewText": "Welcome to {productName} - Let's get started",
"heading": "Welcome to {productName}",
"hello": "Hi {userName},",
"mainText": "Thanks for signing up! We're excited to have you on board. Click the button below to access your dashboard and get started.",
"ctaButton": "Go to Dashboard"
}
```
### 3. Export the Template
Add the export to the package's index file:
```tsx {% title="packages/email-templates/src/index.ts" %}
export { renderInviteEmail } from './emails/invite.email';
export { renderAccountDeleteEmail } from './emails/account-delete.email';
export { renderOtpEmail } from './emails/otp.email';
export { renderWelcomeEmail } from './emails/welcome.email'; // Add this
```
### 4. Use the Template
```tsx
import { getMailer } from '@kit/mailers';
import { renderWelcomeEmail } from '@kit/email-templates';
async function sendWelcome(user: { email: string; name: string }) {
const { html, subject } = await renderWelcomeEmail({
userName: user.name,
productName: 'Your App',
dashboardUrl: 'https://app.example.com/dashboard',
});
const mailer = await getMailer();
await mailer.sendEmail({
to: user.email,
from: process.env.EMAIL_SENDER!,
subject,
html,
});
}
```
## Reusable Components
MakerKit provides styled components for consistent email design:
### EmailWrapper
The outer container with proper styling:
```tsx
<EmailWrapper>
{/* Email content */}
</EmailWrapper>
```
### EmailHeader and EmailHeading
Header section with title:
```tsx
<EmailHeader>
<EmailHeading>Your Email Title</EmailHeading>
</EmailHeader>
```
### EmailContent
Main content area with white background:
```tsx
<EmailContent>
<Text>Your email body text here.</Text>
</EmailContent>
```
### CtaButton
Call-to-action button:
```tsx
<CtaButton href="https://example.com/action">
Click Here
</CtaButton>
```
### EmailFooter
Footer with product name:
```tsx
<EmailFooter>Your Product Name</EmailFooter>
```
## Internationalization
Templates support multiple languages through the i18n system. The language is determined by:
1. The `language` prop passed to the render function
2. Falls back to `'en'` if no language specified
### Adding a New Language
Create a new locale folder:
```
packages/email-templates/src/locales/es/
```
Copy and translate the JSON files:
```json {% title="packages/email-templates/src/locales/es/invite-email.json" %}
{
"subject": "Te han invitado a unirte a {teamName}",
"heading": "Únete a {teamName} en {productName}",
"hello": "Hola {invitedUserEmail},",
"mainText": "<strong>{inviter}</strong> te ha invitado a unirte al equipo <strong>{teamName}</strong> en <strong>{productName}</strong>.",
"joinTeam": "Unirse a {teamName}",
"copyPasteLink": "O copia y pega este enlace en tu navegador:",
"invitationIntendedFor": "Esta invitación fue enviada a {invitedUserEmail}."
}
```
Pass the language when rendering:
```tsx
const { html, subject } = await renderInviteEmail({
// ... other props
language: 'es',
});
```
## Styling with Tailwind
React Email supports Tailwind CSS classes. The `<Tailwind>` wrapper enables Tailwind styling:
```tsx
<Tailwind>
<Body>
<Text className="text-[16px] font-bold text-blue-600">
Styled text
</Text>
</Body>
</Tailwind>
```
Note that email clients have limited CSS support. Stick to basic styles:
- Font sizes and weights
- Colors
- Padding and margins
- Basic flexbox (limited support)
Avoid:
- CSS Grid
- Complex transforms
- CSS variables
- Advanced selectors
## Testing Templates
Please use the Dev Tool to see how emails look like.
## Next Steps
- [Send emails](/docs/next-supabase-turbo/sending-emails) using your templates
- [Configure Supabase Auth emails](/docs/next-supabase-turbo/authentication-emails) with custom templates
- [Test emails locally](/docs/next-supabase-turbo/emails/inbucket) with Mailpit

188
docs/emails/inbucket.mdoc Normal file
View File

@@ -0,0 +1,188 @@
---
status: "published"
title: "Local Email Development with Mailpit"
label: "Local Development"
description: "Test email functionality locally using Mailpit. Capture authentication emails, team invitations, and transactional emails without sending real messages."
order: 4
---
When developing locally, Supabase automatically runs [Mailpit](https://mailpit.axllent.org) (or InBucket in older versions) to capture all emails. This lets you test email flows without configuring an email provider or sending real emails.
## Accessing Mailpit
With Supabase running locally, open Mailpit at:
```
http://localhost:54324
```
All emails sent during development appear here, including:
- Supabase Auth emails (verification, password reset, magic links)
- MakerKit transactional emails (team invitations, account notifications)
- Any custom emails you send via the mailer
## How It Works
The local Supabase setup configures both Supabase Auth and MakerKit to use the local SMTP server:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Your App │────▶│ Local SMTP │────▶│ Mailpit UI │
│ (Auth + Mailer)│ │ (port 54325) │ │ (port 54324) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
The default development environment variables in `apps/web/.env.development` point to this local SMTP:
```bash
EMAIL_HOST=127.0.0.1
EMAIL_PORT=54325
EMAIL_USER=
EMAIL_PASSWORD=
EMAIL_TLS=false
EMAIL_SENDER=test@makerkit.dev
```
## Testing Common Flows
### Email Verification
1. Start your dev server: `pnpm dev`
2. Sign up with any email address (e.g., `test@example.com`)
3. Open [http://localhost:54324](http://localhost:54324)
4. Find the verification email
5. Click the verification link
### Password Reset
1. Go to the sign-in page
2. Click "Forgot password"
3. Enter any email address
4. Check Mailpit for the reset email
5. Click the reset link to set a new password
### Magic Link Login
1. Go to the sign-in page
2. Enter an email and request a magic link
3. Check Mailpit for the magic link email
4. Click the link to sign in
### Team Invitations
1. Sign in and navigate to team settings
2. Invite a new member by email
3. Check Mailpit for the invitation email
4. Use the invitation link to accept
## Using a Real Email Provider Locally
If you need to test with a real email provider during development (e.g., to test email rendering in actual clients), override the development environment variables:
```bash {% title="apps/web/.env.development.local" %}
# Override to use Resend locally
MAILER_PROVIDER=resend
RESEND_API_KEY=re_your-test-key
EMAIL_SENDER=YourApp <test@yourdomain.com>
```
Or for SMTP:
```bash {% title="apps/web/.env.development.local" %}
# Override to use real SMTP locally
EMAIL_HOST=smtp.your-provider.com
EMAIL_PORT=587
EMAIL_USER=your-username
EMAIL_PASSWORD=your-password
EMAIL_TLS=true
EMAIL_SENDER=YourApp <test@yourdomain.com>
```
{% alert type="default" title="Supabase Auth Emails" %}
Even with a custom email provider, Supabase Auth emails (verification, password reset) still go through Mailpit. To test Supabase Auth emails with a real provider, you need to configure SMTP in the Supabase dashboard.
{% /alert %}
## Debugging Email Issues
### Emails Not Appearing in Mailpit
If emails don't show up:
1. **Verify Supabase is running**: Check `supabase status`
2. **Check the port**: Mailpit runs on port 54324, SMTP on 54325
3. **Check environment variables**: Ensure `EMAIL_HOST=127.0.0.1` and `EMAIL_PORT=54325`
4. **Check server logs**: Look for SMTP connection errors in your terminal
### Wrong Email Content
If emails appear but content is wrong:
1. **Check template rendering**: Templates are rendered at send time
2. **Verify template data**: Log the data passed to `renderXxxEmail()` functions
3. **Check i18n**: Ensure translation files exist for your locale
### Links Not Working
If email links don't work when clicked:
1. **Check `NEXT_PUBLIC_SITE_URL`**: Should be `http://localhost:3000` for local dev
2. **Verify route exists**: The link destination route must be implemented
3. **Check token handling**: For auth emails, ensure `/auth/confirm` route works
## Mailpit Features
Mailpit provides useful features for testing:
### Search and Filter
Filter emails by:
- Sender address
- Recipient address
- Subject line
- Date range
### View HTML and Plain Text
Toggle between:
- HTML rendered view
- Plain text view
- Raw source
### Check Headers
Inspect email headers for:
- Content-Type
- MIME structure
- Custom headers
### API Access
Mailpit has an API for programmatic access:
```bash
# List all messages
curl http://localhost:54324/api/v1/messages
# Get specific message
curl http://localhost:54324/api/v1/message/{id}
# Delete all messages
curl -X DELETE http://localhost:54324/api/v1/messages
```
## Production Checklist
Before deploying, ensure you've:
1. **Configured a production email provider**: Set `MAILER_PROVIDER`, API keys, and SMTP credentials
2. **Verified sender domain**: Set up SPF, DKIM, and DMARC records
3. **Updated Supabase Auth templates**: Replace default templates with token-hash versions
4. **Tested real email delivery**: Send test emails to verify deliverability
5. **Set up monitoring**: Configure alerts for email delivery failures
## Next Steps
- [Configure your production email provider](/docs/next-supabase-turbo/email-configuration)
- [Set up Supabase Auth email templates](/docs/next-supabase-turbo/authentication-emails)
- [Create custom email templates](/docs/next-supabase-turbo/email-templates) with React Email

View File

@@ -0,0 +1,273 @@
---
status: "published"
label: "Sending Emails"
description: "Send transactional emails from your Next.js Supabase application using the MakerKit mailer API. Learn the email schema, error handling, and best practices."
title: "Sending Emails in the Next.js Supabase SaaS Starter Kit"
order: 1
---
The `@kit/mailers` package provides a simple, provider-agnostic API for sending emails. Use it in Server Actions, API routes, or any server-side code to send transactional emails.
## Basic Usage
Import `getMailer` and call `sendEmail` with your email data:
```tsx
import { getMailer } from '@kit/mailers';
async function sendWelcomeEmail(userEmail: string) {
const mailer = await getMailer();
await mailer.sendEmail({
to: userEmail,
from: process.env.EMAIL_SENDER!,
subject: 'Welcome to our platform',
text: 'Thanks for signing up! We are excited to have you.',
});
}
```
The `getMailer` function returns the configured mailer instance (Nodemailer or Resend) based on your `MAILER_PROVIDER` environment variable.
## Email Schema
The `sendEmail` method accepts an object validated by this Zod schema:
```tsx
// Simplified representation of the schema
type EmailData = {
to: string; // Recipient email (must be valid email format)
from: string; // Sender (e.g., "App Name <noreply@app.com>")
subject: string; // Email subject line
} & (
| { text: string } // Plain text body
| { html: string } // HTML body
);
```
You must provide **exactly one** of `text` or `html`. This is a discriminated union, not optional fields. Providing both properties or neither will cause a validation error at runtime.
## Sending HTML Emails
For rich email content, use the `html` property:
```tsx
import { getMailer } from '@kit/mailers';
async function sendHtmlEmail(to: string) {
const mailer = await getMailer();
await mailer.sendEmail({
to,
from: process.env.EMAIL_SENDER!,
subject: 'Your weekly summary',
html: `
<h1>Weekly Summary</h1>
<p>Here's what happened this week:</p>
<ul>
<li>5 new team members joined</li>
<li>12 tasks completed</li>
</ul>
`,
});
}
```
For complex HTML emails, use [React Email templates](/docs/next-supabase-turbo/email-templates) instead of inline HTML strings.
## Using Email Templates
MakerKit includes pre-built email templates in the `@kit/email-templates` package. These templates use React Email and support internationalization:
```tsx
import { getMailer } from '@kit/mailers';
import { renderInviteEmail } from '@kit/email-templates';
async function sendTeamInvitation(params: {
invitedEmail: string;
teamName: string;
inviterName: string;
inviteLink: string;
}) {
const mailer = await getMailer();
// Render the React Email template to HTML
const { html, subject } = await renderInviteEmail({
teamName: params.teamName,
inviter: params.inviterName,
invitedUserEmail: params.invitedEmail,
link: params.inviteLink,
productName: 'Your App Name',
});
await mailer.sendEmail({
to: params.invitedEmail,
from: process.env.EMAIL_SENDER!,
subject,
html,
});
}
```
See the [Email Templates guide](/docs/next-supabase-turbo/email-templates) for creating custom templates.
## Error Handling
The `sendEmail` method returns a Promise that rejects on failure. Always wrap email sending in try-catch:
```tsx
import { getMailer } from '@kit/mailers';
async function sendEmailSafely(to: string, subject: string, text: string) {
try {
const mailer = await getMailer();
await mailer.sendEmail({
to,
from: process.env.EMAIL_SENDER!,
subject,
text,
});
return { success: true };
} catch (error) {
console.error('Failed to send email:', error);
// Log to your error tracking service
// Sentry.captureException(error);
return { success: false, error: 'Failed to send email' };
}
}
```
### Common Error Causes
| Error | Cause | Solution |
|-------|-------|----------|
| Validation error | Invalid email format or missing fields | Check `to` is a valid email, ensure `text` or `html` is provided |
| Authentication failed | Wrong SMTP credentials | Verify `EMAIL_USER` and `EMAIL_PASSWORD` |
| Connection refused | SMTP server unreachable | Check `EMAIL_HOST` and `EMAIL_PORT`, verify network access |
| Rate limited | Too many emails sent | Implement rate limiting, use a queue for bulk sends |
## Using in Server Actions
Email sending works in Next.js Server Actions:
```tsx {% title="app/actions/send-notification.ts" %}
'use server';
import { getMailer } from '@kit/mailers';
export async function sendNotificationAction(formData: FormData) {
const email = formData.get('email') as string;
const message = formData.get('message') as string;
const mailer = await getMailer();
await mailer.sendEmail({
to: email,
from: process.env.EMAIL_SENDER!,
subject: 'New notification',
text: message,
});
return { success: true };
}
```
## Using in API Routes
For webhook handlers or external integrations:
```tsx {% title="app/api/webhooks/send-email/route.ts" %}
import { NextResponse } from 'next/server';
import { getMailer } from '@kit/mailers';
export async function POST(request: Request) {
const { to, subject, message } = await request.json();
try {
const mailer = await getMailer();
await mailer.sendEmail({
to,
from: process.env.EMAIL_SENDER!,
subject,
text: message,
});
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to send email' },
{ status: 500 }
);
}
}
```
## Best Practices
### Use Environment Variables for Sender
Never hardcode the sender email:
```tsx
// Good
from: process.env.EMAIL_SENDER!
// Bad
from: 'noreply@example.com'
```
### Validate Recipient Emails
Before sending, validate that the recipient email exists in your system:
```tsx
import { getMailer } from '@kit/mailers';
async function sendToUser(userId: string, subject: string, text: string) {
// Fetch user from database first
const user = await getUserById(userId);
if (!user?.email) {
throw new Error('User has no email address');
}
const mailer = await getMailer();
await mailer.sendEmail({
to: user.email,
from: process.env.EMAIL_SENDER!,
subject,
text,
});
}
```
### Queue Bulk Emails
For sending many emails, use a background job queue to avoid timeouts and handle retries:
```tsx
// Instead of this:
for (const user of users) {
await sendEmail(user.email); // Slow, no retry handling
}
// Use a queue like Trigger.dev, Inngest, or BullMQ
await emailQueue.addBulk(
users.map(user => ({
name: 'send-email',
data: { email: user.email, template: 'weekly-digest' },
}))
);
```
## Next Steps
- [Create custom email templates](/docs/next-supabase-turbo/email-templates) with React Email
- [Build a custom mailer](/docs/next-supabase-turbo/custom-mailer) for other providers
- [Test emails locally](/docs/next-supabase-turbo/emails/inbucket) with Mailpit