Iris is self-hosted transactional mail infrastructure: a single server that takes a message from your system over HTTP, stores it durably, and delivers it through your own Microsoft 365 or Cloudflare account.
It carries the messages a person is waiting for because they just did something: a welcome, a password reset, a one-time code.
What it does
- Accepts mail over a simple API.
POST /t/<tenant>/sendwith a bearer key answers202and a message id once the message is safely stored. - Delivers it reliably. Each message has a tracked life: queued, sending, retrying, then sent or
failed with the reason. Retries back off from 5 seconds to 5 minutes, honour
Retry-After, and run up to 8 attempts. - Makes retries safe. With an
Idempotency-Key, a caller can repeat a request after a timeout and get the original message back. - Survives crashes. An admitted message is sent, or marked failed with the reason. After a
kill -9, Iris takes up unfinished work as soon as it restarts. Delivery is at least once. - Serves many tenants from one deployment. Each tenant has its own store Heap, keys, senders, provider accounts and delivery loop, so a hung provider stalls only the tenant using it.
- Sends the exact bytes it was given. Links, bodies and headers reach the provider as the sending system wrote them, and the wording stays with that system, versioned and reviewed with its code.
- Reports delivery facts. Operators can see each message's state, every attempt, and what the provider said.
Why it matters
- A reset link is a credential. Because Iris carries links unchanged, that credential stays between the sending system, the provider and the person it was sent to.
- Sensitive content has a short life. Bodies, attachments and custom headers are dropped as soon as a message is sent or fails. The record that remains is deleted after a retention period, 30 days by default.
- You keep your sending identity. Mail goes out through your own mailbox or domain, under your own SPF, DKIM and provider relationship.
- Authentication can depend on it. Senders use keys that Iris issues itself, so Iris and an identity service such as Arbax can each start and work independently, and Arbax can hand Iris its own reset and verification mail.
- Transactional reputation stays separate. A marketing product sends through Iris as its own tenant, with its own senders and sending domain, away from password resets.
How it is built
- One Rust binary and one data directory, with the Residiuum database embedded in the process. There is no separate database or queue to run.
- Two APIs: the runtime API for sending systems, and an operator API for tenants, accounts, senders, keys and messages.
- Two providers: Microsoft 365 through Graph for mail from a mailbox, and Cloudflare Email Service for mail from a domain. Each account uses the one that fits its address.
- Any language can call the HTTP API, and Rust callers also have the typed
iris-clientcrate. - Settings are
IRIS_*environment variables. Iris ships with systemd packaging,/healthand/readyendpoints, and structured JSON logs that can be forwarded to Ringtail. - Backups dump the store, prove the dump by restoring it, and can go off-site encrypted.
Security
- API key secrets are 192 random bits, stored as SHA-256 digests. Each key names the senders it may use.
- The operator API runs on TLS 1.3 with client certificates from a dedicated CA, and every change is logged with the certificate's subject and fingerprint.
- Provider secrets are stored wrapped, using AES-256-GCM bound to one tenant, account and credential version, or HashiCorp Vault transit. They are unwrapped in memory to send.
- Logs name messages by id and recipients by domain. Addresses, bodies and secrets stay out of them.
- Per-tenant send limits, by default 120 a minute and 10,000 a day, answer
429withRetry-After.
Measured
Iris runs well on a small machine. In its soak test, Iris had a 320 MiB memory budget and one CPU, shared with the test's own load, and sent 50,000 messages across five tenants to a stand-in provider, with 16 concurrent senders:
| Admission | 499 messages a second |
| Worst p95 admission latency | 103 ms |
| Delivery of all 50,000 | 563 seconds, about 89 a second, no duplicates |
| Peak anonymous memory | 290 MiB |
A separate full run added a deep queue of 10,000 messages, all delivered, and a kill -9
mid-delivery with nothing lost and a restart in under 4 seconds.
Compared with other options
| Iris | Hosted email APIs (SendGrid, Postmark, Mailgun, Amazon SES) | A mail server (Postfix) | Marketing platforms (Mailchimp) | |
|---|---|---|---|---|
| Runs on | Your server | The vendor's platform | Your server | The vendor's platform |
| Sends through | Your Microsoft 365 or Cloudflare account | The vendor's sending network | Your own IPs and reputation | The vendor's sending network |
| Interface | HTTP API with idempotent admission | HTTP API and SMTP | SMTP | Web app and API |
| Where mail content lives | In your system; in Iris only until sent | Your system, and often vendor templates and logs | Your system and the mail queue | The platform's templates and campaigns |
| Built for | One-to-one transactional mail | Transactional, and often bulk | Mail transport of all kinds | Campaigns to audiences |
| Multi-tenancy | Built in, with isolated storage and delivery | Vendor sub-accounts | Configured by hand | Vendor accounts |
| Cost shape | Your server, plus your provider's sending | Usually per message or plan | Your server and deliverability work | Usually per contact or plan |
Vendor features and pricing change; check them before relying on this table.
Choose Iris when you want a mail API with durable delivery and tenant isolation, running on infrastructure you control and sending under a mail identity you already own.
Where it fits in Frogfish
Iris is the mail layer of the Frogfish ecosystem. Arbax composes its authentication
mail and hands it to Iris, and Nurve, a marketing application, sends through it as its own tenant.
Iris is live at send.frogfish.io.