Store: the full Postgres schema as an embedded migration. UUIDv7 keys so `ORDER BY id` is a free chronological index; raw MIME and attachments live in object storage with only a key in the row; `pods` present from day one because retrofitting tenancy costs more than an unused column. API keys are stored as a SHA-256 hash — a database dump must not be a set of live credentials. API: the v0 route table, including `ingest`, which closes the receive→thread→extract loop with zero mail infrastructure and is what makes the agent layer testable in CI. Scopes are a closed enum rather than strings, so "can send mail" and "can mint keys" are not one typo apart. Internal errors are logged in full and reported as a bare string. MCP: the tool catalogue, six tools. Adding a row here is the only way an agent gains a capability — a new REST route is invisible until someone opts it in. Three tests guard the rule that no tool can ever reach key management; CI fails rather than production. ADR 0006: enterprise self-hosted first. A hosted offering comes only after we have run this ourselves long enough to have a deliverability record worth selling. `pods` stays in the schema as the thing that keeps that path open — do not remove it as dead code. Also: multi-stage Dockerfile running as a non-root system user with no shell in the runtime image, and the openmail.karti.ai static page. 17 tests, zero clippy warnings, fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
40 lines
1.9 KiB
Markdown
40 lines
1.9 KiB
Markdown
# ADR 0006 — Enterprise self-hosted first; SaaS only after we run it ourselves
|
|
|
|
**Status:** Accepted, 2026-09-02.
|
|
|
|
## Decision
|
|
|
|
The product is a **self-hostable server for enterprises, small businesses and
|
|
builders.** A hosted offering is not a v1 goal and may never exist. If it does,
|
|
it comes only after we have run OpenMail ourselves, in production, long enough
|
|
to have a deliverability track record worth selling.
|
|
|
|
## Why this ordering and not the reverse
|
|
|
|
- **Deliverability cannot be shortcut.** A hosted product's entire value is
|
|
inbox placement, which is months of IP warmup, feedback-loop enrolment and
|
|
suppression-list discipline. Selling that before we have it is selling
|
|
something we do not own.
|
|
- **Self-hosted is the differentiator.** Every competing agent-mailbox product
|
|
is hosted-only. Leading with a SaaS puts us on their ground, competing on the
|
|
thing they have already spent years on.
|
|
- **Auditability is the purchase condition.** Enterprises will not point MX at
|
|
a closed box. Public source under Apache-2.0 is why they can say yes, and the
|
|
self-hosted path is the one that requires no trust in us at all.
|
|
- **We become our own first serious user.** The bugs that matter in mail —
|
|
silent DANE downgrades, an MTA-STS policy cached wrong for a year — surface
|
|
only under real traffic. Running it ourselves before selling it is how we
|
|
find them on our own mail instead of a customer's.
|
|
|
|
## Consequences
|
|
|
|
- `pods` (tenancy) stays in the schema from v0.1 even though nothing uses it.
|
|
It is the piece that keeps the SaaS path open without a migration. **Do not
|
|
remove it as dead code.**
|
|
- No billing, no Stripe, no hosted control plane in the tree. When someone
|
|
proposes one, this ADR is the answer.
|
|
- Docs, defaults and error messages are written for an operator running one
|
|
box, not for a tenant of ours.
|
|
- The website (`website/`) sells self-hosting and links to the repo. It does
|
|
not collect signups for a product that does not exist.
|