You're reading docs for Nextly Alpha. APIs may change between releases.

Guides

Email

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.providerConfig is required when the email block is set. If you only want database-managed providers, you can omit the email block entirely.

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

OptionTypeDefaultDescription
providerConfigobjectrequiredProvider-specific configuration.
fromstringrequiredDefault sender address.
baseUrlstringNEXT_PUBLIC_APP_URL envBase URL for links in emails.
resetPasswordPathstring'/admin/reset-password'Path for password-reset links.
verifyEmailPathstring'/admin/verify-email'Path for email-verification links.

Provider resolution order

When sending an email, Nextly resolves the provider in this order:

  1. Specific provider ID -- if the call passes a providerId.
  2. Database default provider -- the provider marked as default in admin Settings.
  3. Code-first config -- the providerConfig from defineConfig().
  4. 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:

TemplateSlugPurpose
WelcomewelcomeSent after user registration.
Password Resetpassword-resetContains the reset link.
Email Verificationemail-verificationContains 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}}.

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 HTML

Layout 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 mailpit

This 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.local

Then 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

VariableProviderDescription
RESEND_API_KEYResendAPI key from the Resend dashboard.
SENDLAYER_API_KEYSendLayerAPI key (Bearer token).
SMTP_HOSTSMTPServer hostname.
SMTP_PORTSMTPServer port (587 for TLS, 1025 for Mailpit).
SMTP_USERSMTPAuthentication username (empty for Mailpit).
SMTP_PASSSMTPAuthentication password (empty for Mailpit).
SMTP_FROMAnyDefault sender address.

Next steps