Configure email providers (Resend, SMTP, SendLayer), send programmatically via the Direct API, and test locally with Mailpit.
Nextly's email system supports three providers -- Resend, SMTP, and SendLayer -- with database-managed templates, variable interpolation, and a shared header/footer layout. Email is used for password resets, email verification, welcome messages, and any custom notifications you send via the Direct API.
For local development, Nextly ships a Mailpit profile in docker-compose.yml so you can test email flows without sending anything over the internet -- see Local email testing with Mailpit below.
Provider configuration
Configure your email provider in nextly.config.ts as a code-first fallback. Database-managed providers (configured in the admin UI at Settings -> Email Providers) take priority over code-defined config.
Note:
email.providerConfigis required when the
Resend
// nextly.config.ts
import { defineConfig } from "@nextlyhq/nextly/config";
export default defineConfig({
email: {
providerConfig: {
provider: "resend",
apiKey: process.env.RESEND_API_KEY!,
},
from: "My App <noreply@example.com>",
},
});SMTP
// nextly.config.ts
export default defineConfig({
email: {
providerConfig: {
provider: "smtp",
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT) || 587,
secure: false,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
},
from: process.env.SMTP_FROM || "noreply@example.com",
},
});SendLayer
// nextly.config.ts
export default defineConfig({
email: {
providerConfig: {
provider: "sendlayer",
apiKey: process.env.SENDLAYER_API_KEY!,
},
from: "My App <noreply@example.com>",
},
});Conditional provider selection
The playground app shows a useful pattern -- prefer Resend when its API key is set, fall back to SMTP otherwise:
// nextly.config.ts
export default defineConfig({
email: process.env.RESEND_API_KEY
? {
providerConfig: {
provider: "resend" as const,
apiKey: process.env.RESEND_API_KEY,
},
from: process.env.SMTP_FROM || "onboarding@resend.dev",
}
: {
providerConfig: {
provider: "smtp" as const,
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT) || 587,
secure: false,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
},
from: process.env.SMTP_FROM || "noreply@example.com",
},
});Email config options
| Option | Type | Default | Description |
|---|---|---|---|
providerConfig | object | required | Provider-specific configuration. |
from | string | required | Default sender address. |
baseUrl | string | NEXT_PUBLIC_APP_URL env | Base URL for links in emails. |
resetPasswordPath | string | '/admin/reset-password' | Path for password-reset links. |
verifyEmailPath | string | '/admin/verify-email' | Path for email-verification links. |
Provider resolution order
When sending an email, Nextly resolves the provider in this order:
- Specific provider ID -- if the call passes a
providerId. - Database default provider -- the provider marked as default in admin Settings.
- Code-first config -- the
providerConfigfromdefineConfig(). - Error -- if none of the above resolve.
This lets you wire a code-first provider for development and configure a production provider through the admin UI without changing code.
Built-in email templates
Nextly automatically creates three email templates on first startup:
| Template | Slug | Purpose |
|---|---|---|
| Welcome | welcome | Sent after user registration. |
| Password Reset | password-reset | Contains the reset link. |
| Email Verification | email-verification | Contains the verification link. |
Templates are stored in the database and edited through the admin UI (Settings -> Email Templates) using a Lexical-based rich-text editor. They support {{variable}} placeholder syntax with HTML escaping.
Template variables
Templates receive context-appropriate variables.
Password Reset:
{{resetLink}}-- full password reset URL.{{userName}}-- user's name or email.{{userEmail}}-- user's email.{{appName}}-- application name.{{expiresIn}}-- token expiry duration.{{year}}-- current year.
Email Verification:
{{verifyLink}}-- full verification URL.{{userName}},{{userEmail}},{{appName}},{{expiresIn}},{{year}}.
Welcome:
{{userName}},{{userEmail}},{{appName}},{{year}}.
Shared header / footer layout
Templates can opt into a shared layout via the useLayout flag (enabled by default). The layout uses reserved slugs _email-header and _email-footer. The final email is composed as:
header HTML + template body + footer HTMLLayout templates also support {{variable}} interpolation, so common variables like {{year}} and {{appName}} work in headers and footers.
Code-first template overrides
You can override the built-in email templates in defineConfig() without touching the database:
// nextly.config.ts
export default defineConfig({
email: {
providerConfig: { /* ... */ },
from: "noreply@example.com",
templates: {
passwordReset: (data) => ({
subject: "Reset your password",
html: `
<p>Hi ${data.user.name ?? data.user.email},</p>
<p>Click <a href="${data.url}">here</a> to reset your password.</p>
<p>This link expires in 1 hour.</p>
`,
}),
welcome: (data) => ({
subject: `Welcome to our app!`,
html: `<p>Hi ${data.user.name}, thanks for signing up!</p>`,
}),
emailVerification: (data) => ({
subject: "Verify your email",
html: `<p>Click <a href="${data.url}">here</a> to verify your email.</p>`,
}),
},
},
});Resolution order for templates: database template (if active) > code-first override > error.
Sending emails programmatically
The Direct API exposes nextly.email.send() and nextly.email.sendWithTemplate().
Using a template
import { nextly } from "@/nextly";
await nextly.email.sendWithTemplate({
template: "password-reset",
to: "user@example.com",
variables: {
resetLink: "https://example.com/admin/reset-password?token=abc123",
userName: "Jane",
appName: "My App",
},
});Sending raw
await nextly.email.send({
to: "user@example.com",
subject: "Your order has shipped",
html: "<p>Your order #1234 is on its way!</p>",
});Both calls return { success: boolean; messageId?: string }.
Auth flow auto-sends
The AuthService handles email sending for auth flows automatically when an email service is configured:
generatePasswordResetToken(email)-- generates a token and sends the reset email.generateEmailVerificationToken(email)-- generates a token and sends the verification email.sendWelcomeEmail(to, user)-- sends the welcome template.
If no email service is configured, tokens are returned in the API response as a development-only fallback (with a console warning). In production, configure a real provider so password resets and email verification work.
Local email testing with Mailpit
For local development, the Nextly repo's docker-compose.yml ships a with-mailpit profile that runs Mailpit -- an SMTP server and web UI for catching outgoing email locally.
Start Mailpit
docker compose --profile with-mailpit up -d mailpitThis starts the axllent/mailpit:latest container with:
- SMTP on host port
1025. - Web UI on host port
8025. - No authentication (Mailpit is dev-only).
Configure Nextly to use it
Point your SMTP env vars at the local Mailpit instance. Empty SMTP_USER / SMTP_PASS is fine -- Mailpit accepts any credentials.
# .env.local
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USER=
SMTP_PASS=
SMTP_FROM=dev@nextly.localThen either let getStorageFromEnv()-style auto-detection pick up SMTP, or wire it explicitly in nextly.config.ts:
// nextly.config.ts
import { defineConfig } from "@nextlyhq/nextly/config";
export default defineConfig({
email: {
providerConfig: {
provider: "smtp",
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT) || 1025,
secure: false,
auth: {
user: process.env.SMTP_USER || "",
pass: process.env.SMTP_PASS || "",
},
},
from: process.env.SMTP_FROM || "dev@nextly.local",
},
});Inspect captured email
Open http://localhost:8025 in your browser. Every email Nextly sends locally lands in the Mailpit inbox -- password resets, verification links, welcome emails, and anything you fire via nextly.email.send(). The web UI lets you preview HTML, view raw source, and inspect headers.
The container is dev-only and gated behind a Compose profile, so it does not auto-start with docker compose up.
Environment variables
| Variable | Provider | Description |
|---|---|---|
RESEND_API_KEY | Resend | API key from the Resend dashboard. |
SENDLAYER_API_KEY | SendLayer | API key (Bearer token). |
SMTP_HOST | SMTP | Server hostname. |
SMTP_PORT | SMTP | Server port (587 for TLS, 1025 for Mailpit). |
SMTP_USER | SMTP | Authentication username (empty for Mailpit). |
SMTP_PASS | SMTP | Authentication password (empty for Mailpit). |
SMTP_FROM | Any | Default sender address. |
Next steps
- Authentication -- understand how the auth flows trigger emails.
- Deployment -- production email-provider setup.
- Environment variables -- full env-var reference.
- Configuration reference -- full
defineConfig()email options.