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 "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: {
provider: "resend",
apiKey: process.env.RESEND_API_KEY!,
},
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 }.
Email to a person a plugin signs in
Needs the release after 0.0.2-alpha.66.
A plugin emailing one of its own signed-in people
passes person to ctx.services.email.send or sendWithTemplate. The delivery
record then names the person, so the plugin's report that they were deleted erases
it, and leaves alone the records of anyone else at the same address.
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 "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.