Email¶
Pebbl services send outbound email through AWS SES using mail.pebblfinance.com as the MAIL FROM subdomain.
Inbound mail is managed by Google Workspace (MX records on the apex domain are defined in the pebbl-email Terraform module).
Sending¶
All outbound email comes from no-reply@pebblfinance.com and goes through the outbox pipeline:
- A service writes an outbox entry (e.g., user signs up, submits a contact form, logs in from a new device).
- The event-dispatcher polls the outbox table and pushes entries to the email SQS queue.
- The email-event-consumer Lambda pulls the message, loads the matching MJML template from S3, renders it with Handlebars, converts it to HTML, and sends it through SES.
Per-event (per-email) overrides for subject lines, recipients, and CC/BCC are set through the EMAIL_OVERRIDES environment variable on the Lambda. As is idiomatic in Pebbl, these are defined in Terraform wherever the email-event-consumer module is consumed.
Email templates¶
Templates are written as Python code using a component DSL (Header, Text, Button, Footer, etc.). The DSL outputs .mjml files with Handlebars placeholders.
Regenerate after editing:
Commit the generated files and merge into main. The CI will upload these to S3 and the assets can be promoted to staging and production via the pebbl-assets-promote.yml job.
| Template | Trigger |
|---|---|
welcome_email |
Account creation (delayed 5 min) |
contact_form_submitted |
POST /contact |
mailing_list_new_subscriptions |
Mailing list subscribe |
account_marked_for_deletion |
User initiates deletion |
account_deleted |
Scheduled task after grace period |
new_login |
New device fingerprint |
developer_notification |
Internal alerts |
transactions_ready |
Not wired up yet |
Email branding rules¶
- Headers: Kanit
- Body: Inter
- Buttons: purple CSS gradients (a solid-color gradient tricks dark mode into preserving the original color instead of inverting it)
- Footer: dark background (dark mode still inverts it on some clients, no reliable fix yet)
Image assets
All image assets referenced in email templates should be hosted on the CDN so that email clients can fetch them reliably.
Adding a new email¶
- Define a new
EmailTemplatein the component DSL. - Add a matching
OutboxEventPayloadsubclass in the Python basebedrockand a TypeScript payload type in the Typescriptbase. - Add the event type to
PebblOutboxEvent.EventType. - Add the event type to the
event-dispatcherrouting config (in Terraform infrastructure where the event-dispatcher module is consumed) so the event can be routed to the email queue. - Generate the new template, merge into main, and promote to staging and production if needed.
Now you may dispatch an email event matching your new type.
Receiving¶
Routing for shared addresses (support@, legal@, security@, etc.) is done through aliases and groups in Google Workspace Admin, not DNS or Terraform. To add a new address, create an alias or group there.
Authentication¶
The pebbl-email Terraform module manages all email DNS records (MX, SPF, DKIM, DMARC, BIMI) through Cloudflare.
- SPF:
v=spf1 include:amazonses.com include:_spf.google.com ~all. Covers both SES and Google. - DKIM: Three CNAME records for SES. Google Workspace DKIM is set up separately in the admin console.
- DMARC:
v=DMARC1; p=none; sp=none; adkim=r; aspf=r;. Monitor-only for now, should move toquarantineorrejectonce DKIM alignment is confirmed for both SES and Google. - BIMI: Record exists for brand logo display but needs a Verified Mark Certificate (VMC) to actually work.