Email deliverability
Log every email the app sends, read bounces and complaints from SES or Resend webhooks, and block suppressed addresses before they reach the provider.
Email deliverability keeps the sending reputation healthy. Every email the send-email worker handles - transactional and marketing - gets a row in the email log, provider webhooks move it to delivered, bounced or complained, and bounced or complaining addresses land on a suppression list that every later send is checked against. It works with no setup on any provider; the webhooks below add delivery events.
The email log
Each send is one email_sends row. The recipient is stored only as a SHA-256 hash of the lowercased address.
| Status | Meaning |
|---|---|
queued | Logged, not yet handed to the provider. |
sent | The provider accepted it. |
delivered | The provider reported delivery (SES, Resend). |
bounced | A permanent bounce. The address is suppressed. |
complained | The recipient marked it as spam. The address is suppressed. |
failed | Every retry failed; error holds the reason. |
skipped | Blocked by the suppression list; error is suppressed:bounce or suppressed:complaint. |
- A status only moves forward, so a late delivery event never hides a bounce.
- A transient (soft) bounce is recorded as a
soft_bounceevent without blocking the address. - The log is pruned after
config.email.tracking.retentionDays(default90); suppressions are never pruned.
Blocking rules
The worker checks email_suppressions before every provider call.
| Reason | Transactional (password reset, verification) | Marketing (founder templates) |
|---|---|---|
bounce | Blocked | Blocked |
complaint | Sent | Blocked |
A new bounce or complaint also sets the account's marketingOptIn to false, so newsletter sync drops it too.
Set up SES events
AMAZON_SES_CONFIGURATION_SET to its name - every send is tagged with it.AMAZON_SES_SNS_TOPIC_ARN to the topic's ARN and deploy.https://<api-host>/webhooks/email/ses (under your API base path). The endpoint confirms the subscription itself. Leave raw message delivery off.- Messages are accepted only from that topic, with a valid SNS signature from the topic's own region, and at most 72 hours old.
- A daily job also copies SES's account-level suppression list into
email_suppressions, catching addresses SES suppressed without reporting them. - The SES credentials need
ses:SendEmail,ses:ListSuppressedDestinations(daily sync) andses:DeleteSuppressedDestination(admin unblock).
Set up Resend events
https://<api-host>/webhooks/email/resend with the email.delivered, email.bounced and email.complained events.RESEND_WEBHOOK_SECRET to the webhook's signing secret (whsec_...) and deploy.SMTP has no delivery events. Sends stay sent, and the suppression list fills only from SES
or Resend events.
The admin Emails page
/admin/emails shows deliverability in every build, whether or not open/click tracking is on.
| Tab | What it shows |
|---|---|
| Overview | Sent, delivered, bounce rate and complaint rate over the last 90 days, then open and click performance. A rate is marked at risk from 2% bounces or 0.1% complaints. |
| Log | Every send with recipient, template, subject, status and error. Search one exact address, filter by status, load more by cursor. |
| Blocked | The suppression list with reason and source. Unblock asks for confirmation, then calls the delete route below. |
AWS reviews SES sending accounts past roughly 5% bounces or 0.1% complaints, and can pause sending. Act on an at-risk card before it gets there.
Admin API
All routes sit behind adminGuard under /admin.
| Route | Purpose |
|---|---|
GET /admin/emails/deliverability | Delivery totals and bounce/complaint rates, with optional startDate / endDate. Not gated on tracking. |
GET /admin/emails/log | Email log, newest first, 50 per page. Filters: email (exact address), status, userId; pass nextCursor back as cursor. |
GET /admin/emails/suppressions | Suppressed addresses, with search, limit, offset. |
DELETE /admin/emails/suppressions/:email | Unblock an address. On SES it also lifts the account-level suppression; the action is written to the audit log. |
GET /admin/email-analytics | Also returns the same deliverability totals beside Email analytics when tracking is on. |