Skip to content

Email Delivery ​

Transactional email (invitations, password resets, verification) goes out through Amazon SES from organisation-consumer. Inbound mail to @schemastack.io is handled separately by Cloudflare Email Routing. Confusing the two costs hours — most of this page exists because it did.

Path out ​

organisation-consumer (quarkus-mailer, SMTP)
  → email-smtp.eu-north-1.amazonaws.com:587  (STARTTLS, SMTP credentials)
    → SES  → recipient

Configuration is entirely environment-driven (quarkus.mailer.* in organisation-consumer/application.properties), so the values live in .env.production.local on the server, never in the repo:

VariableNote
QUARKUS_MAILER_HOSTRegion-specific — must match the region where the domain is verified
QUARKUS_MAILER_USERNAME / _PASSWORDSMTP credentials, not an AWS access key/secret — generated in the SES console and only shown once
QUARKUS_MAILER_FROMMust be on a verified domain, and should align with the MAIL FROM domain for DMARC
QUARKUS_MAILER_START_TLSREQUIRED on port 587

quarkus.mailer.keep-alive=false is deliberate: SES drops idle SMTP connections, and a pooled dead connection surfaces as an intermittent send failure.

The failure that looks like nothing at all ​

An address on the SES account-level suppression list is silently dropped. SES accepts the message, returns a message ID, and the application logs a successful send. Nothing is queued, nothing retries, nothing errors. The recipient simply never receives it, and every layer you would normally check reports success.

Addresses land there automatically after a bounce — including bounces from long before the domain was properly configured. That is the trap: the address was suppressed during an earlier broken setup, the setup was then fixed, and the address stayed suppressed. It reads exactly like "email is still broken" when in fact only that one recipient is.

Check it first, before touching DNS, DKIM, or anything else:

bash
aws sesv2 get-suppressed-destination --email-address user@example.com --region eu-north-1
aws sesv2 list-suppressed-destinations --region eu-north-1

# Remove once the underlying cause is understood
aws sesv2 delete-suppressed-destination --email-address user@example.com --region eu-north-1

The tell that distinguishes this from a domain-wide problem: other recipients receive mail normally, and the same recipient receives mail sent from anywhere other than SES. If a test to you+tag@gmail.com arrives while the real address does not, stop looking at DNS.

Diagnose with a fresh address

Send to an address that has never bounced (you+something@gmail.com). If it arrives, delivery works and the problem is per-recipient. This takes seconds and rules out the entire DNS surface.

Authentication (all three must align) ​

Configured once for schemastack.io; recorded here because the failure modes are indistinguishable from each other and from suppression.

  • DKIM — Easy DKIM, three CNAME records. Verify in the SES console that it reads Successful and Enabled; the CNAMEs existing in DNS is not sufficient.
  • Custom MAIL FROM — mail.schemastack.io, needing an MX and an SPF TXT record. Without it the envelope sender is an amazonses.com subdomain, which passes SPF but does not align with the From domain — so DMARC fails on SPF and rests entirely on DKIM.
  • DMARC — one TXT record at _dmarc.schemastack.io. Alignment is what it checks; publishing the record while MAIL FROM is unaligned makes things worse, not better.

Confirm from a delivered message's headers rather than from the console — Authentication-Results should show spf=pass, dkim=pass, and dmarc=pass, and the header.from / smtp.mailfrom domains should match.

Verification is per region

A domain verified in eu-central-1 is not verified in eu-north-1. Identities, DKIM, MAIL FROM, and the suppression list are all regional. Sending through a region where the domain is not verified fails even though the console shows a healthy, verified domain — you are looking at a different region's page. Match QUARKUS_MAILER_HOST to the region where verification actually succeeded.

Inbound is a different system ​

careers@schemastack.io, synthetics@schemastack.io and similar are Cloudflare Email Routing rules that forward to a personal mailbox. This has nothing to do with SES, and reasoning across the two is what makes this area confusing.

Two things to know:

  • Destination addresses must be verified in Cloudflare before forwarding works.
  • Forwarding rejects unauthenticated mail with 550 5.7.26. A probe you send by hand from an unauthenticated client will be refused, which measures your own sender — not the application's.

That last point is worth stating plainly: testing inbound routing tells you nothing about whether SES can deliver. Test outbound by triggering a real invitation, and read the consumer's logs:

bash
docker logs organisation-consumer-blue 2>&1 | grep -i "mail\|invitation"

A successful send logs a message ID. A message ID and no delivery means suppression — go back to the top of this page.

SchemaStack Internal Developer Documentation