Tenant isolation is enforced by the database
Every table that belongs to a customer carries an account id and is covered by a PostgreSQL row-level security policy keyed on it, with FORCE enabled so the policy applies to the table owner too. The application connects as a role that cannot bypass those policies.
This matters because it makes isolation a property of the database rather than of every query we will ever write. A developer who forgets a WHERE account_id = … gets no rows, not somebody else’s.
Message bodies are encrypted at rest
The text and HTML body of every message, and the composed, signed copy of the message exactly as it went out on the wire, are sealed with AES-256-GCM before they are written to the database — a fresh random nonce per value, an authentication tag that makes a tampered row fail to open rather than open wrongly, and a key that is held outside the database and supplied to the running processes at startup.
Be clear about the scope, because a vaguer sentence would be a more flattering one. What is sealed is the body. What is not sealed is everything the delivery record is indexed and searched by: the subject line, the sender and recipient addresses, the recipient domain, your custom headers, Reply-To and List-Unsubscribe. Those are stored as ordinary columns, and they are the columns a waybill is made of.
And be clear about the threat. This defends against the database being read without the application: a stolen dump, a leaked backup, a restore handed to the wrong person, an operator with a database password and a psql prompt. It does not defend against the application host itself being compromised, because the key has to be present in the process that composes and signs your mail. Encryption at rest is a boundary around the database, not a claim that nobody could ever read anything.
We cannot read your mail without telling you
Sealing the body stops it being read by holding a database credential. It does not stop it being read by the one route that is allowed to — abuse reports and support tickets both need that route to exist. So the promise is not “we never look”, which would not survive the first spam complaint. It is that looking cannot be silent.
Opening a stored message body is a single deliberate action, and it does three things:
- It requires a written reason of at least ten characters. The rule lives in the database function itself, not in the admin screen, so it cannot be skipped by calling the API directly. Ten characters is a longer minimum than any other operator action, because “ticket” is not a reason to read somebody’s mail.
- It writes an audit entry in the same database transaction that reads the row — who read it, which message, when, from which address, and the reason they gave. There is no ordering of events in which the body comes out and the record does not.
- It emails the account owner, naming the message, the recipient, the time, the member of staff, and the reason verbatim. Not a summary: the sentence the operator typed is the sentence you read, which is what keeps those sentences honest. It cannot be turned off.
Two honest limits on that. The notice is sent immediately after the read rather than as part of it, deliberately — our own mail server being down must not roll back an abuse investigation — so the durable record is the audit entry, and the email is what carries it to you. And the notice goes to the account owner, so an account with no owner on it is a case where the read is still recorded but nobody is written to.
The rest of the operator panel is built so that this stays the only door. The screens that list an account’s messages do not join the table the bodies live in, so they are not capable of showing one; and mail received at an address belonging to a customer is withheld from staff outright — there is no break-glass for it at all.
Signing keys are encrypted at rest
Each sending domain gets its own 2048-bit DKIM key pair, generated by us. The private half is encrypted with AES-256-GCM under a key held outside the database and mounted at runtime, never baked into a container image or committed to a repository.
The reason is specific: a database dump alone must not let anyone sign mail as one of our customers. That would let them forge DMARC-passing mail from a domain we vouch for, which is worse than leaking the data itself.
API keys are never stored
We store an HMAC of the secret under a server-side pepper, not the secret. A key is shown once, at creation. If it is lost it is rotated, not recovered — and a stolen database gives an attacker nothing they can authenticate with.
The delivery record is append-only
Message events are written to a log that cannot be updated or deleted, and each entry is hash-linked to the one before it. The chain is computed inside the database, in the same transaction as the insert, so two concurrent writers cannot fork it.
The practical effect is that the record is tamper-evident: removing or editing an event breaks the chain from that point onward, and you can verify it yourself rather than taking our word for it.
There is exactly one sanctioned way for an event to leave that log, and it is retention. Expiry runs through a database function that is the only thing permitted to lift the block — and it writes a row saying what it removed and how far back it went, so a gap in a chain is a gap with a signed explanation next to it rather than a mystery.
Two other logs are built the same way and held to a stricter standard still. The billing ledger and the operator audit log are hash-chained and append-only and the application’s database role has had the privilege to insert into them taken away entirely — every entry has to come through a specific, named function. That closes a hole the delivery log does not close: a chained log that anyone may append to will happily compute a perfectly valid hash over a forged entry.
Webhooks are signed and replay-resistant
Every webhook carries a timestamped HMAC signature over the raw body. Verify it with a constant-time comparison and reject anything outside your tolerance window — the docs show exactly how, including the mistake of parsing the JSON before verifying the bytes.
Deliveries are retried with backoff and carry an idempotency key that stays the same across retries, so a redelivery can never be mistaken for a second event.
In transit
The API is HTTPS only. Outbound mail uses opportunistic TLS: we negotiate STARTTLS with the receiving server whenever it offers it, which is the overwhelming majority of mail. Where a receiver offers no TLS the alternative is not delivering at all, so we deliver and record exactly what happened — the connection details are in the message’s record.
Backups
The database is dumped nightly and dumps are kept for fourteen days. A restore is rehearsed rather than assumed — there is a script whose only job is to load the most recent dump into a scratch database and check it came back.
This is the practical reason bodies and signing keys are sealed rather than merely access-controlled: a backup file is the copy of your data most likely to end up somewhere nobody intended. It is also the reason a deletion is not instantaneous everywhere. When something is deleted from the live database it is gone from it immediately, and gone from the backups as those roll off within fourteen days.
What we do not claim
Everything above is the whole of it. We hold no certification of any kind — not SOC 2, not ISO 27001 — and no third party has audited any of it. There is no formal bug bounty. We are not going to imply otherwise with a badge or a phrase like “enterprise-grade”, and we are not going to put a date on this page for when that changes, because we have not decided.
What we offer instead is the page you have just read: specific controls, described precisely enough to be checked, with their limits stated next to them. If you are evaluating us and need more detail on any of them, write to [email protected] and ask — we would far rather have that conversation now than during procurement. And if what you need is an audited provider today, we are not it.
Reporting a vulnerability
Email [email protected] with enough detail to reproduce the issue. We will acknowledge within three working days and keep you updated until it is resolved.
Please do not run automated scanners against production, do not access or modify data that is not yours, and give us a reasonable window to fix things before disclosing publicly. We will not pursue legal action against anyone acting in good faith under those terms.
- Security contact
- [email protected]
- Abuse reports
- [email protected]
- Data protection
- [email protected], and the data processing terms