How it works
One request in. One message out, as written.
Your system makes one HTTP request. Iris checks it, stores it, and delivers it through the provider your sender uses, retrying until the provider accepts it or it is certain it never will.
Sending
A sending system holds an API key that Iris issued for one tenant, naming the senders it may use. It gives a recipient, a subject and a body, plain text, HTML or both, and Iris answers 202 Accepted with a message id: queued, not yet delivered.
- Idempotent. Send an
Idempotency-Keywith the request, and a retry after a timeout returns the message the key first admitted instead of admitting another. - Exactly as written. Iris escapes nothing and rewrites nothing in a body. Addresses, subjects and custom headers are refused if they contain line breaks, and custom headers must be
X-headers. - Errors you can act on. A refusal is
application/problem+jsonwith atracenaming the check that refused it, such asiris-key-invalidoriris-sender-not-allowed.
A message’s life
Each tenant has its own delivery loop inside the Iris process. Admitting a message wakes it at once. It claims each message with compare-and-swap before sending, so two workers never send the same message, and records each attempt together with the message’s next state in one atomic write.
- At least once. An admitted message is sent, or marked failed with the reason. A process that dies after the provider accepted a message but before recording it sends it again on restart; the opposite trade would lose mail silently.
- Bounded. Each provider call times out (30 s by default) and each attempt has a 90 s deadline.
- Retried with judgement. Throttling, outages, timeouts and dropped connections are retried, a few times within an attempt and then between attempts, from 5 seconds doubling to 5 minutes, honouring
Retry-After. A refusal such as 400 or 403 fails at once. After eight attempts the message fails, with every attempt kept. An account’s policy can change any of this. - Isolated. A slow or hung provider stalls only its own tenant.
Providers
| Provider | For |
|---|---|
| Microsoft 365, through Microsoft Graph | Mail from a real mailbox, where replies and bounces should land in an inbox people read. |
| Cloudflare Email Service | Mail from a domain, such as noreply@ codes and confirmations. |
They are complementary, not failover for each other. SPF, DKIM and MX records bind an address to the provider that sends it, so each sender goes through exactly one.
Tenants, keys and operators
- Tenants share nothing. Each has its own storage, keys, senders and delivery loop. A key of one tenant means nothing to another, and an unknown tenant looks exactly like an unknown route.
- Keys are for machines. A key’s secret is 192 random bits, stored only as a digest. A key may send as the senders it names, and nothing else.
- Operators prove who they are. The operator API answers only over TLS 1.3 to a client certificate from your own operators’ CA. Every change is logged with the certificate’s subject and fingerprint. There is no web console to attack.
Secrets
- Provider credentials are wrapped. Iris refuses one in the clear. A credential is sealed with AES-256-GCM to one tenant, account and version, or held in HashiCorp Vault’s transit engine, and opened in memory only to send.
- The unseal key stays off the host. Iris starts sealed and serves every tenant it already has. The unseal key is needed only to add a tenant, and is handed over on a file descriptor, never through the environment or a file.
- Bodies are short-lived. A message’s bodies, attachments and custom headers are dropped as soon as it is sent or fails. Its recipient, subject and attempts stay for troubleshooting, and the whole record is deleted after a retention period, 30 days by default.
- Logs name messages, not people. One JSON line per event, with recipients by domain only. Addresses, bodies and secrets are never logged.
What Iris does not promise
- Delivery to the inbox. “Sent” means the provider accepted the message. Where it lands is up to the receiving mail system and your domain’s reputation.
- Exactly once. At least once, so a crash at the wrong moment can send a message twice. Idempotency keys stop your retries from doing the same.
- Strict order. Messages go roughly oldest first, but a retry can fall behind a later message.
- Bulk volume. Iris is sized for transactional mail: plan on thousands of messages a day per tenant, not millions.