Stage 3, step 3. Written by Sonnet from the code and the plan, checked
by Opus.
- guide.md: a section on the panel - menu, paths, the roles global and
domain with outbound and inbound assigned separately -, Overview and
Health, Queue and System log, Account and Server > Settings, the
domain page / Domain settings / application form, Help as a page of
topics. The DMARC report address is one of three choices set in
Domain settings. The Spam filter box, the two INBOUND_ANTISPAM_*
variables the panel reads, and the filter's socket on Health.
Sessions; backup and export encrypted unless the box is unticked;
1.x data is not restored into 2.0.
- architecture.md: Panel HTTP surface rewritten from web.go (routes,
the /server/ guard, per-object checks, <page>/fragment, one layout,
typed page data, the kit and its guards, CSP and htmx); Persistence
gains the database - one baseline, a 1.x file refused, the tables as
they are.
- security.md: roles and what each reaches, as built; sessions by user
id and what ends them; state-changing requests; errors shown to
users; the 2.0 review; two accepted risks from it - a delegated
inbound upstream may be any host and port, and Account's profile is
saved without the password.
- CHANGELOG: Removed opens with "2.0 starts from an empty data
directory"; entries that describe the user-default report address
say they were replaced before release; the filter's socket.
- roadmap.md: two review follow-up rows struck as retired by the
redesign (.pair / .split, the two back links); password-reset notes
that changing the account e-mail must then ask for the password.
Progress 25/27.
- development.md: the component kit and its guards; README and
product.md: wording.
Left as they are
- The third follow-up row the plan expected to strike - initShowWhen
is not re-run after an htmx swap - still holds: no polled fragment
carries such a control today, so nothing breaks, but the redesign did
not retire it.
- guide.md embeds no screenshots, so none needed retaking.
- README and guide still name image 1.9.5 - the version cut.
- plans/preflight.md and plans/inbound-antispam-panel.md use pre-2.0
paths; roadmap's template-data-typing is largely done by the
redesign.
go test -count=1 ./cmd/panel (env-doc tables): ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.9 KiB
SelfPost — product boundaries
What this file is. Stable product definition for SelfPost v1.0: purpose, deployment assumptions, explicit out-of-scope items, and the multi-domain model. User-facing overview in README.md; install and operations in guide.md; as-built technical detail in architecture.md.
Purpose
SelfPost is a self-hosted SMTP relay with a web control panel, shipped as a single Docker image. It sends mail directly from your server with per-domain DKIM signing; an optional inbound relay forwards mail on port 25 without storing it.
Primary workflow: configure the relay once (domains, DKIM, SASL applications), then point scripts and apps at the SMTP endpoint. SelfPost delivers as the configured sending domain.
Positioning
| Text | |
|---|---|
| Stamp subtitle | SELF-HOSTED SMTP RELAY |
| Wordmark | The stamp's own letter outlines without the stamp (docs/assets/selfpost-wordmark.svg, -ink for light grounds) — for places too small for the stamp, such as the panel header |
| Tagline | YOUR SMTP. YOUR RULES. |
| Hero line (README, GitHub) | Self-hosted SMTP relay with a web panel — one Docker image, no provider. |
Brand assets and metrics: docs/assets/selfpost-proof.html.
Deployment context (fixed assumptions)
These constraints are intentional; changing them requires an explicit product decision:
- VPS or home server — one image for both. Raspberry Pi and similar boards are not a current target but not ruled out forever.
- Send from your own IP (DIY) — no intermediate relay provider. Requires outbound port 25, static IP, and configurable PTR/rDNS from the operator.
- Single container — Postfix, OpenDKIM, and the panel run under one
supervisordinside one image. - Panel is internet-facing — mandatory security requirements in security.md.
Infrastructure the operator provides (not SelfPost features): unblocked outbound TCP 25, static IP, PTR/rDNS, acceptable IP reputation. SelfPost does not detect, bypass, or compensate for missing prerequisites; mail simply fails to deliver when they are absent.
Out of scope
Explicitly excluded to prevent scope creep:
- Inbound mail (IMAP/POP3, mailboxes, delivery to user inboxes)
- Webmail
- Organisations / tenancy beyond the
globalanddomainroles; managing multiple sending domains under one global administrator is in scope (see below) - Inbound antispam/antivirus engines (rspamd, ClamAV, etc.) — SelfPost may expose a milter hook; it does not ship or start a filter
- A custom MTA — Postfix is used as-is
- Dovecot or a full mail stack for SASL — Cyrus SASL (
sasldb2) only
The domain role ships in the current line (a global user plus
domain users with assigned domains, outbound and inbound separately). The
optional inbound relay (backup-MX / forwarder on port 25) ships in [1.4.0], off by default behind
INBOUND_RELAY_ENABLE; it is relay/forward, not IMAP/webmail. Send-log
retention in Settings ships in [1.5.0]. 30-day send statistics and
auto level-2 rate limits ship in [1.6.0]. DMARC aggregate report
ingest ships in [1.7.0], off by default behind DMARC_REPORTS_ENABLE.
Items marked candidate in the
roadmap require explicit approval before coding.
Multi-domain model
SelfPost is a multi-domain outbound relay. Two linked entities:
Sending domain
Example: example.com. Has its own DKIM key and selector.
Application (account)
A SASL login/password bound to one domain. A domain may have several applications (e.g. newsletter vs alerts), each with its own credentials, but each may send only from its own domain, never from another.
From-address mode (per application)
Set when creating or editing an application:
- Any address in the domain —
*@example.com(wildcard insmtpd_sender_login_maps). - Explicit address list — only listed From addresses within the domain; anything else is rejected even if it belongs to the same domain.
In both modes the From address must belong to the application's domain.
What the panel manages per domain
- DKIM key + selector (shared by all applications on that domain)
- One or more applications (SASL credentials + From mode + optional rate limits)
- DNS guidance (DKIM TXT; reminders for SPF/DMARC)
Adding a domain does not create an application automatically.
Lifecycle
- Add domain — record, DKIM key, DNS instructions; no application yet.
- Add application — SASL pair (password shown once), From mode, map entries,
postfix reload. - Delete domain — removes DKIM key and all its applications.
- Delete application — removes only that app's SASL and map entries.
This is not multi-tenancy; it is one owner (or a small team with global and domain-scoped roles) operating several sending domains with independent application credentials.