Pro Emails
Viewing latest docs.
Switch version: v3

Emails

Transactional emails

Open up email.ex and you will see a list of functions that generate Swoosh email structs. Eg:

elixir
def confirm_register_email(email, url) do
  base_email()
  |> to(email)
  |> subject("Confirm instructions")
  |> render_body("confirm_register_email.html", %{url: url})
  |> premail()
end

If I run this function in IEX I can see the Swoosh struct:

A Swoosh.Email struct can be delivered to an email address by a Swoosh mailer (see mailer.ex). Eg:

elixir
MyApp.Email.confirm_register_email(user.email, url)
|> MyApp.Mailer.deliver()

So email.ex creates the Swoosh structs and the functions for actually delivering emails like the previous code example are in user_notifier.ex. Think of email.ex functions like view templates, and user_notifier.ex functions like controller actions.

The steps to creating a new email:

  1. Create the function to generate a Swoosh struct in email.ex
  2. Ensure it looks good by navigating to http://localhost:4000/dev/emails and creating a new action in email_testing_controller.ex that renders the html_body value of the struct
  3. Create a new user_notifier.ex function for use in your application

To see a more detailed write up on creating an email — see this section in the “Complete web app” guide.

Email provider: Resend

Petal Pro v4 uses Resend as the sole transactional email provider via the Swoosh.Adapters.Resend adapter. AWS SES has been removed.

Configuration

Set the RESEND_API_KEY environment variable (get it from your Resend dashboard):

shell
fly secrets set RESEND_API_KEY="re_..."

In runtime.exs the mailer is configured automatically when the key is present:

elixir
config :petal_pro, PetalPro.Mailer,
  adapter: Swoosh.Adapters.Resend,
  api_key: System.fetch_env!("RESEND_API_KEY")

No MAIL_PROVIDER env var is needed — Resend is the only option.

Email template components

Petal Pro ships a set of HEEx email layout components you can compose to build consistent, well-styled transactional emails. These live in lib/petal_pro_web/components/email_components.ex.

Configurable section gap

The gap between sections in the email body is configurable via a gap assign on the wrapper component:

heex
<.email_body gap="32px">
  <.email_section>First section</.email_section>
  <.email_section>Second section</.email_section>
</.email_body>

Gray button variant

In addition to the default primary button, a gray variant is available for secondary actions (e.g. “Unsubscribe”, “View in browser”):

heex
<.email_button href={@url} variant="gray">
  Manage preferences
</.email_button>

Callout boxes

Use callout boxes to draw attention to important information — warnings, confirmation codes, or key instructions:

heex
<.email_callout>
  Your confirmation code is <strong>482901</strong>. It expires in 15 minutes.
</.email_callout>

Callout boxes render with a subtle background and border, making them stand out from surrounding body text.

Marketing emails with Resend

Marketing emails are emails you wish to send to your whole subscriber base or some segment of them to push a marketing or product update. They differ from transactional emails in that you don’t program them in code — you compose them in the Resend dashboard and hit send.

We use Resend for marketing because it has a clean API, generous free tier, and a good unsubscribe / deliverability story out of the box. Transactional email continues to go through Swoosh + AWS SES — these are two separate concerns.

How sync works

Contacts (your users) sync to Resend automatically:

  • On sign-in, profile update, notification settings change, onboarding completion, and membership expiry, an Oban job (PetalPro.Workers.ContactSyncWorker) runs PetalPro.MarketingEmail.sync_contact/1
  • That call upserts the user as a Resend contact (email, first name, unsubscribed flag) and reconciles their segment membership
  • Suspended or deleted users are removed from Resend entirely

To backfill existing users (e.g. after first deploy), run:

elixir
# In iex
PetalPro.MarketingEmail.sync_all_contacts()

# Or as a Mix task
mix petal_pro.sync_to_resend

Each user gets its own Oban job with random 0-600s jitter to stay under Resend’s default 2 req/sec rate limit.

Segments

Resend slices contacts via segments (formerly “audiences”). We use three:

Env var Members
RESEND_SEGMENT_ID_ALL_USERS All confirmed, non-suspended, non-deleted users with setting_notification_subscribe_newsletter = true
RESEND_SEGMENT_ID_MEMBERS Active Petal Pro members
RESEND_SEGMENT_ID_EXPIRED_MEMBERS Users whose membership has expired and who have no active membership

A user can be in multiple segments simultaneously (a current member is in both ALL_USERS and MEMBERS). On every sync we read the contact’s current segments from Resend, diff against the desired set, and add/remove explicitly via POST/DELETE /contacts/:id/segments/:segment_id.

To add a new segment:

  1. Create the segment in the Resend dashboard, capture its ID
  2. Add a config key in config/runtime.exs
  3. Add a clause in PetalPro.MarketingEmail.desired_segments/1

Inbound webhook

Resend sends events to POST /webhooks/resend (handled by PetalProWeb.ResendWebhookController). Signatures are verified via svix HMAC against RESEND_WEBHOOK_SECRET.

Event Action
contact.updated with unsubscribed: true Flips setting_notification_subscribe_newsletter to false on the matching user
email.bounced Logged to Logs for review
email.complained Logged to Logs for review (spam complaints damage sender reputation)

Required env vars

bash
RESEND_API_KEY=re_...
RESEND_SEGMENT_ID_ALL_USERS=...
RESEND_SEGMENT_ID_MEMBERS=...
RESEND_SEGMENT_ID_EXPIRED_MEMBERS=...
RESEND_WEBHOOK_SECRET=whsec_...