---
title: Email
description: "How Feeblo sends mail. The SMTP transport variables, the Amazon SES feedback webhook, and the outbox controls."
---

Feeblo sends every kind of mail through one mailer: verification and password reset
links, onboarding and user-feedback digests, weekly summaries, and notifications. The
server records each message in an outbox table and a worker inside the server process
delivers it, so a failed send retries instead of vanishing.

Sign-up blocks on email by default: new accounts must click a verification link
(`AUTH_EMAIL_VERIFICATION_REQUIRED` defaults to true). Get SMTP working before you open
registrations, or nobody gets in.

## Transports

The repo documents three transport names in `.env.example`:

| Value         | Meaning                                          |
| ------------- | ------------------------------------------------ |
| `smtp-auth`   | default. SMTP with username and password         |
| `smtp-api`    | documented option, no implementation in this source |
| `resend`      | documented option, no implementation in this source |

:::note[Only the SMTP transport ships today]
The mailer implementation in `packages/transactional/` is a nodemailer transport
configured entirely by the `SMTP_*` variables below. Nothing in the current source reads
`SMTP_TRANSPORT`, so the value is forward-looking: set it to `smtp-auth` (the default)
and configure the SMTP variables.
:::

## SMTP variables

| Variable                    | Default        | Notes                                                        |
| --------------------------- | -------------- | ------------------------------------------------------------ |
| `SMTP_HOST`                 | `127.0.0.1`    | Mail server host                                             |
| `SMTP_PORT`                 | `2500`         | The dev stack uses `1025` (Mailpit)                          |
| `SMTP_USERNAME`             | unset          | Auth is only attempted when this is set                      |
| `SMTP_PASSWORD`             | unset          | Paired with `SMTP_USERNAME`                                  |
| `SMTP_SECURE`               | `false`        | `true` forces TLS on the connection                          |
| `SMTP_UNSAFE_IGNORE_TLS`    | `false`        | `true` skips TLS even when the server offers STARTTLS        |
| `SMTP_SERVICE`              | unset          | Well-known nodemailer service name (for example `gmail`)     |
| `SMTP_FROM_ADDRESS`         | `hello@feeblo.com` | The from address for all mail                             |
| `SMTP_FROM_NAME`            | unset          | Listed in `.env.example`, but no code reads it; setting it does nothing |
| `SMTP_PERSONAL_FROM_ADDRESS`| unset          | Sender for onboarding and user-feedback mail; falls back to `SMTP_FROM_ADDRESS` |

TLS behavior follows nodemailer semantics. With `SMTP_SECURE=true`, the connection
upgrades to TLS up front. With `SMTP_SECURE=false` (default) the client uses STARTTLS
when the server offers it, and `SMTP_UNSAFE_IGNORE_TLS=true` disables even that. Leave
the ignore flag alone unless your relay serves plaintext only.

The from-address trio deserves attention before you launch:

- `SMTP_FROM_ADDRESS` appears on every message unless a template overrides it.
- `SMTP_PERSONAL_FROM_ADDRESS` gives personal lifecycle mail (onboarding, user
  feedback) its own sender. Unset, it falls back to `SMTP_FROM_ADDRESS`.
- Skip `SMTP_FROM_NAME`: it sits in `.env.example` as "required", but nothing in the
  codebase reads it.

:::tip[Local mail does not need a relay]
The dev stack runs Mailpit (`axllent/mailpit`) instead of a real SMTP server. The
compose file maps its SMTP port `1025` and its web UI on `http://localhost:8025`, where
you can read every message the server "sent". Point `SMTP_HOST=127.0.0.1`,
`SMTP_PORT=1025`, and leave credentials empty.
:::

## Outbox controls

Delivery pauses and cost guards are environment-driven so an operator can react without
redeploying. All five variables are optional and live in `packages/domain/src/email-outbox/`:

| Variable                                | Default     | Meaning                                                  |
| --------------------------------------- | ----------- | -------------------------------------------------------- |
| `EMAIL_OUTBOX_GLOBAL_DELIVERY_PAUSED`   | `false`     | `true` stops all outbound delivery immediately           |
| `EMAIL_OUTBOX_MAX_CONCURRENT_SENDS`     | `10`        | Worker concurrency; clamps to at least 1                 |
| `EMAIL_OUTBOX_MONTHLY_SEND_LIMIT`       | `100000`    | Monthly send budget; clamps to at least 1                |
| `EMAIL_OUTBOX_ESTIMATED_SEND_COST_MICROS` | `100`     | Assumed cost per send in microdollars for cost tracking  |
| `EMAIL_OUTBOX_PAUSED_WORKSPACE_IDS`     | empty       | Comma-separated workspace IDs whose mail is paused       |

Set `EMAIL_OUTBOX_GLOBAL_DELIVERY_PAUSED=true` while you migrate providers or fix a
bounced-sender problem. Messages keep queuing in the outbox, and delivery resumes when
you flip the flag back. `EMAIL_OUTBOX_PAUSED_WORKSPACE_IDS` pauses a single tenant the
same way; the server splits the comma-separated list and trims whitespace.

## Amazon SES feedback webhook

Feeblo can ingest Amazon SES delivery events: deliveries, bounces, and complaints.
Two variables switch it on:

- `EMAIL_PROVIDER_WEBHOOK_TOKEN` activates the webhook at `POST /email-provider/ses/:token`.
  Generate it with `openssl rand -hex 32`. The token must not be derivable from
  `AUTH_ENCRYPTION_KEY`, so a dedicated random value is required.
- `EMAIL_PROVIDER_SNS_TOPIC_ARN` filters incoming messages by their signed SNS topic.
  Set it whenever the token is set: the webhook rejects messages whose `TopicArn` does
  not match, and without it the source is unverifiable, so requests get rejected anyway.

Subscribe your SNS topic to:

```
https://<API_URL>/email-provider/ses/<EMAIL_PROVIDER_WEBHOOK_TOKEN>
```

Leave both variables unset and the route stays inert (404). SNS sends its notifications
over a publicly reachable HTTPS URL, so point the topic subscription at the server's
public `API_URL`.

## Next steps

<CardGroup cols={2}>
  <Card title="Environment variables" href="/self-hosting/environment">
    Every variable Feeblo reads, grouped and marked required or optional.
  </Card>
  <Card title="Database" href="/self-hosting/database">
    Postgres specifics and the migration lifecycle.
  </Card>
  <Card title="Media" href="/self-hosting/media">
    S3-compatible object storage for uploads.
  </Card>
  <Card title="Troubleshooting" href="/self-hosting/troubleshooting">
    Symptom-to-cause fixes for common failure modes.
  </Card>
</CardGroup>