# 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](#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

```typescript
// 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

```typescript
// 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

```typescript
// 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:

```typescript
// 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:

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:

| 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 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:

```typescript
// 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

```typescript
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

```typescript
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](https://nextlyhq.com/docs/plugins/signed-in-people.md#email-a-person)
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](https://github.com/axllent/mailpit) -- an SMTP server and web UI for catching outgoing email locally.

### Start Mailpit

```bash
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.

```bash
# .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`:

```typescript
// 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](https://nextlyhq.com/docs/guides/authentication.md) -- understand how the auth flows trigger emails.
* [Deployment](https://nextlyhq.com/docs/guides/deployment.md) -- production email-provider setup.
* [Environment variables](https://nextlyhq.com/docs/configuration/environment.md) -- full env-var reference.
* [Configuration reference](https://nextlyhq.com/docs/configuration/nextly-config.md) -- full `defineConfig()` email options.
