Overview

Outbound email goes through Resend. Every send is rendered server-side from a template in core.email_templates, addressed using your per-store sender, signed with the per-user signature of whoever triggered it, dispatched via the send-email edge function, and logged with delivery / open / bounce status from the resend-webhook handler.

Failures are visible. Hard bounces auto-suppress so you can’t accidentally hammer a dead inbox. Soft bounces retry with backoff. Suppressions are per-org and visible in Settings → Email log → Suppressions.

What working correctly looks like

  • DNS for your sending domain shows green checks for SPF, DKIM, and Return-Path under Settings → Email → Domain.
  • Sending a test from Settings → Email → Send test lands at the test address within ~30 seconds.
  • Every send shows up in Settings → Email log with a status that progresses queued → sent → delivered.
  • A delivered_at and (when applicable) opened_at timestamp appears on each row within minutes.
  • Hard bounces append the recipient to Settings → Email log → Suppressions automatically.
  • Magic-link emails from auth-email-hook arrive in <30 seconds.

Setup — DNS, DKIM, SPF

Before you can send from billing@yourbrand.com, you’ll add four DNS records. Resend’s domain verification is the gate.

  1. Go to Settings → Email → Domain.
  2. Click Add domain and enter the apex of the address you want to send from (e.g. yourbrand.com).
  3. Add the four records we show you to your DNS provider (Cloudflare, Route53, Namecheap, GoDaddy):
    • 1 TXT for SPF (v=spf1 include:resend.net ~all).
    • 2 CNAMEs for DKIM (resend._domainkey, resend2._domainkey).
    • 1 MX for the Return-Path / bounce address.
  4. Click Verify. Propagation is usually under 10 minutes; can take up to 48 hours.
DMARC is recommended, not required

If your domain already has a DMARC record, leave it. If not, start at v=DMARC1; p=none; rua=mailto:dmarc@yourbrand.com for monitoring before tightening to quarantine. We won’t fight an existing strict policy.

The full operator-side write-up with screenshots lives at Email setup walkthrough.

Per-store sender

Sender configuration is per-store. Settings → Email while the target store is active in the switcher.

  • From address — the verified address recipients see (e.g. orders@retail-brand.com).
  • From name“Retail Brand — Orders”.
  • Reply-to — defaults to the from address; override to route replies to a help-desk inbox.
  • Footer text — small print appended to every send (legal address, unsubscribe note for marketing).

Each store can use a different sending domain. The pipeline picks up the right sender based on which store the trigger fired from.

Templates

Templates live in core.email_templates. Each template is a Mustache-style HTML body plus a subject line, keyed by a stable key. Default templates ship with the platform and can be overridden per-org.

Default templates

  • proposal_sent — the proposal you sent a client is ready for review.
  • proposal_approved — client approved a proposal; sent to the operator.
  • proposal_declined — client declined a proposal; sent to the operator.
  • invoice_sent — a new invoice is ready to pay.
  • invoice_reminder — invoice is approaching / past due.
  • payment_received — receipt sent to the client.
  • refund_issued — refund confirmation.
  • order_shipped — carrier + tracking number.
  • order_delivered — carrier delivery webhook fired.
  • magic_link — sign-in / invite link (delivered via auth-email-hook).
  • password_reset — password-reset link.
  • mfa_enrolled — new MFA factor was added to your account.
  • billing_quota_warning — 50% / 80% / 100% quota notices.
  • billing_payment_failed — Stripe payment failed; grace period started.
  • team_invite — you’ve been invited to a workspace.

Per-org overrides

  1. Go to Settings → Email → Templates.
  2. Click the template you want to override.
  3. Edit the subject and body. The left side is your edit; the right side renders a live preview with sample tokens.
  4. Save. The override stores a row keyed by (org_id, key); the platform falls back to the default if you ever delete the override.
Don’t remove tokens from auth templates

Auth templates (magic_link, password_reset, mfa_enrolled) include tokens like {{ConfirmationURL}} and {{Token}}. Removing them silently breaks sign-in. The editor warns you, but it does not block save — ship a test send before relying on an edited auth template in production.

Per-user signatures

Email triggered by an operator (a proposal sent from Orders, a reply from the inbox) appends that operator’s signature.

  1. Each member: Settings → Profile → Signature.
  2. Edit the HTML signature (image, name, title, phone). The editor inlines images automatically.
  3. Save. The signature is rendered into {{Signature}} on any template that uses it.

System emails (auth, billing, webhooks) don’t use a personal signature — they sign off with the org’s footer text from per-store sender.

Email log

Settings → Email log is the receipt for every send.

  • Statusqueued, sent, delivered, opened, bounced, complained, suppressed.
  • Timestampsqueued_at, sent_at, delivered_at, opened_at, bounced_at.
  • Recipient, subject, template key, store.
  • Provider message id — clickable to open the entry in Resend’s console.
  • Bounce reason — populated from the Resend webhook on bounce.

The log is retained for 365 days. Older rows are archived to cold storage with the audit archive cron.

Bounce handling & suppression

The resend-webhook edge function listens for delivery events and writes them back to your log.

  • Hard bounce (mailbox doesn’t exist, domain unreachable) → recipient is appended to core.email_suppressions immediately. Future sends to that address are suppressed at queue time, never dispatched.
  • Soft bounce (full mailbox, transient 4xx) → we retry with exponential backoff up to 24 hours. After the 4th retry, we mark bounced but do not suppress.
  • Spam complaint → recipient suppressed immediately and the audit log records a email.complaint_received event.

Suppression list

Settings → Email log → Suppressions shows every suppressed address with the reason and the timestamp. To unsuppress (only do this if you’ve confirmed the original cause is gone):

  1. Find the row.
  2. Click Remove from suppression list.
  3. Confirm. The next send to that address is dispatched normally.
Don’t un-suppress to dodge a hard bounce

If a domain hard-bounced, the address is dead. Sending again hurts your domain reputation. Un-suppress only when the recipient asks you to and confirms they fixed their mailbox.

Inbound replies

If you set the inbound webhook on your sending domain, replies sent to the from-address (or to a per-thread reply-to) flow back through resend-webhook and thread into the right place:

  • Replies to a proposal email thread back into the proposal’s message timeline.
  • Replies to an order email thread into the order’s message timeline.
  • Replies to a generic reply-to (e.g. support@) land in the workspace inbox under the matched client.

The reply-to address used on the outbound email determines threading. If you change it mid-thread, replies break threading and land in the inbox without a parent. See Clients → Conversations for the inbox itself.

Settings & permissions

  • Settings → Email — per-store sender, domain, signature footer. Owner / admin.
  • Settings → Email → Templates — per-org template overrides. Owner / admin.
  • Settings → Email log — the send log + suppression list. Visible to all roles for sends in their granted stores; suppression management is admin-only.
  • Settings → Profile → Signature — every member edits their own.

API + automation

The send-email edge function is internal. To send programmatically, fire one of the platform events that maps to a template (e.g. transition an order into shipped to fire order_shipped, post a payment to fire payment_received).

For one-off transactional sends from your own automation, hit the events endpoint and let our pipeline render and dispatch. There is no public “send arbitrary email” endpoint by design — that would let bad actors borrow our domain reputation. If you need it, write to {{CONTACT_EMAIL}}.

Troubleshooting

SymptomLikely causeFix
“Domain not verified”SPF / DKIM / Return-Path not propagatedClick Re-check on Settings → Email → Domain; if still red after 1 hour, re-paste the records.
Email queued but never sentResend account paused or out of quotaCheck Settings → Billing for our quota; check Resend status.
Email sent but never deliveredRecipient’s server greylistingWait 5–15 min; if still nothing, check the bounce log on the row.
Recipient says they got nothing, log says deliveredSpam folderHave them whitelist the from-address; check for missing DMARC alignment.
All sends to one domain bouncingThat domain blocks our IP rangeSuppression handles it. If you must reach them, ask them to whitelist resend.com.
Magic-link emails missingauth-email-hook not configured or signature mismatchRun a test sign-in; if blank, escalate to {{CONTACT_EMAIL}}.

The full symptom-to-fix table lives at Troubleshooting → Email not delivered.