Skip to content

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:

  1. A service writes an outbox entry (e.g., user signs up, submits a contact form, logs in from a new device).
  2. The event-dispatcher polls the outbox table and pushes entries to the email SQS queue.
  3. 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:

just generate-email-templates

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

  1. Define a new EmailTemplate in the component DSL.
  2. Add a matching OutboxEventPayload subclass in the Python base bedrock and a TypeScript payload type in the Typescript base.
  3. Add the event type to PebblOutboxEvent.EventType.
  4. Add the event type to the event-dispatcher routing config (in Terraform infrastructure where the event-dispatcher module is consumed) so the event can be routed to the email queue.
  5. 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 to quarantine or reject once 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.