Security model
Security
Keelpost is an email client for a domain you control, in private development, with no production users. This page describes how the system is built and what it deliberately refuses to do. It is not a compliance page, and the last section says plainly what we do not claim.
One mailbox, one store
Each mailbox is its own SQLite-backed Durable Object. Messages, threads and mailbox settings live in that mailbox’s own database, and attachment blobs live in object storage belonging to the deployment. There is no shared table holding every workspace’s mail side by side, because there is no shared mail table at all.
The shared control-plane database holds accounts, workspaces, mailbox ownership and grants. It does not hold your message bodies.
Every request that touches mail names a mailbox. The server checks your grant on that mailbox, then opens that mailbox and no other. Cross-mailbox reads are not a permission we forgot to check; there is no code path that opens two mailboxes to answer one question.
Roles and grants
There are four roles: owner, admin, operator and viewer. Owners and admins can see the mailboxes belonging to their workspace. Operators and viewers see nothing by default and must be granted access to a specific mailbox.
Membership is invitation-only. There is no self-serve join, no “anyone with an address at this domain” rule, and no open sign-up anywhere in the product.
Revocation is the part most systems get wrong. Removing a grant is not a flag that takes effect at the next login. The next action from that session — a folder open, a search, an agent turn, a tool call — checks the live grant and fails. A user who is looking at a mailbox when their access is removed cannot keep using it.
The agent inherits this exactly. It runs as you, with your grants, re-checked on every turn and every tool call. It has no ambient service identity that could read a mailbox you cannot.
Approval before every state change
The agent can read, search, summarise, triage and draft on its own. It cannot change anything on its own.
Any action that changes state — moving a message, sending, deleting, marking as read — halts and renders an approval card. The card is server-rendered from the stored record: the actual message, and the actual change that will be applied. It is not the model’s account of what it is about to do, which means a confused or manipulated agent still cannot show you a card that misdescribes the action.
Approving is what executes the action, and the permission check runs again at that moment. Denying does nothing at all.
The assistant that drafts replies to incoming mail is narrower still: it has one capability, writing a draft. There is no send in that path, so no failure of judgement in it can deliver a message.
There is no configuration option that disables approval, and no per-action or per-sender allow-list that bypasses it.
Move an email
Proposed by the agent
From the server
- msg_7Kd2QpXr
- Sender
- billing@northharbour.example
- Recipient
- you@yourdomain.example
- Subject
- Invoice 4471 — February
- Body preview
- Please find invoice 4471 attached…
- Current folder
- Inbox (inbox)
- Current state
- Unread
- Attachments
- 1
revision 7 · re-verified
From the model
- Requested destination
- Accounts (accounts)
Inboxmoved to Accounts
Shown from the stored message, not from the agent’s summary. Re-checked against the server the moment you click.
Prompt-injection scanning
Inbound mail is hostile input. Anyone can write to your address, and anyone writing to your address can try to write instructions to your agent.
Before the drafting model sees a message, the message is scanned for prompt injection. If the scan flags it, automatic drafting for that message is blocked. If the scan itself errors, the message is treated as if it had been flagged — the system fails closed rather than fails open. The exact text handed to the scanner is the same text handed to the drafting model, capped at a fixed byte budget, so nothing larger or different slips past the check downstream.
Know exactly what the scan gates: automatic drafting, and only that. A flagged message is not quarantined. It stays in your mailbox and stays readable, so if you ask the agent to read or summarise it, or an MCP client does, its content reaches the model like any other message. The scan removes the case where hostile mail steers the agent with nobody in the room; it does not remove the case where you point the agent at hostile mail yourself.
This reduces a class of attack. It does not eliminate it. Treat agent output the way you treat any other summary of an untrusted document.
You keep the parts that matter
You bring the domain. You bring the Cloudflare account and the routing configuration. You bring the keys. Keelpost is the client on top of infrastructure you already control, which means the strongest revocation available to you is one you perform yourself: change the routing and mail stops arriving here, without asking us.
Be precise about where that boundary sits. What is yours is the domain, the DNS records, the Email Routing rules and the credentials. What is ours is where the mail then lands: mailbox storage, attachment storage and model inference all sit inside the Cloudflare account boundary of the deployment, not inside yours.
The agent runs on Cloudflare Workers AI inside that same account boundary as the rest of the deployment. Message content goes to that model when you use the agent or when auto-drafting runs. There is no other model provider in the path, and no third-party analytics, script, font or image origin on this website.
The MCP endpoint
Keelpost exposes 14 tools over OAuth 2.1 with PKCE: list_mailboxes,
list_emails, get_email, get_thread,
search_emails, create_draft, update_draft,
draft_reply, discard_draft, send_email,
send_reply, mark_email_read, move_email,
delete_email.
Each is gated by scope — mailboxes:read, mail:read,
mail:draft, mail:manage, mail:send,
mail:delete — and on top of the scope, a token still cannot exceed the
mailbox grants of the account that authorised it. Clients are created
and managed by an owner or admin, who can disable, rotate or delete them and revoke consent.
Dynamic client registration is deliberately turned off, so no client can register itself.
Be clear about the difference from the in-app agent, because it does not run the way you
would expect. An MCP client is stopped by scope, and then stopped again. The six tools that
would change something — send_email, send_reply,
move_email, mark_email_read, delete_email and
discard_draft — refuse on every mailbox. A scope proves which client is
calling; it does not prove a person saw the change, and MCP has no confirmation round-trip
we can put a card into yet. So a client you granted mail:send still cannot
send.
That leaves eight tools that work: listing mailboxes, reading, threading, searching, and the three that write drafts. A draft is inert — you review it and send it from the app. The refusal is the default and the failure mode: a mailbox is mutable over MCP only if its settings record says so, nothing in the product writes that record, and a missing, unreadable or incomplete one refuses. Grant the smallest scope set that does the job regardless; the default set for a managed client is read-only.
What we do not claim
We have no certifications, and we are not pursuing a badge to put on this page today. No SOC 2 Type I or Type II. No ISO 27001. No HIPAA, no PCI, no FedRAMP. No third-party audit, no penetration test report, no bug bounty programme.
We have no production users, no uptime record and no SLA. Nothing in this product has been operated at scale, because it has not been operated at all outside development.
Sending is not verified. The outbound path is built to persist a message before publishing any job, and to retry with dead-letter queues, but it has not been proven against a live provider, and until it has we will not describe it as working.
We have no customers, no case studies and no reviews to point you at. If you find any of those on a page claiming to be us, it is not us.
If a specific control matters for your decision, ask before you assume. We would rather answer “not built” than let you infer that it is. The rest of the ledger — what is built and what is not — is on the roadmap.
Join the waitlist
If this is the shape of thing you want between an agent and your mail, leave an address. Sign-ups are closed while we build; the list gets one email, when they open.
We store the address you type, plus a hashed form of your IP address so the form can be rate-limited. One email when sign-ups open, and nothing after that. We do not share the list, and there is no analytics or third-party script on this site.
Also worth reading: what is built and what is not, and the FAQ.