Hermetic NixOS foundation for Surmount mail and web services. Replaces an offline Synology DiskStation (MailPlus + static sites) with a reproducible stack:
- Stalwart - SMTP / submission / IMAP / ManageSieve / JMAP (mail engine
pinned in
nix/packages/stalwart-mail.nix, currently 0.16.15; RocksDB all-role co-location fine for now; docs/DATASTORES.md. Internal FTS good enough for now; Surmount builds own search product later) - HTTPS edge - TLS and routing (nginx transitional-to-delete; target Axum-first edge + Unix domain sockets in docs/EDGE_AND_TLS.md; cert path not locked to ACME-only)
- surmount-management-ui - Rust Axum + Leptos SSR (invested path; embedded HTML bridge today): admin first, then real webmail in v1
- Nostr product auth - keys via host OS / other Surmount tools (Stalwart keeps mail credentials; Vaultwarden for human secret UX)
- Deploy secrets (sops-nix today) - available at NixOS activation on the host (bucket 1); never in public git (plain or ciphertext); planned Vaultwarden for humans (bucket 2); prefer LUKS2 FDE (passphrase / initrd SSH / TPM unlock; unlock material never in git)
- restic - backup options (opt-in); clever RPO/RTO later
- No Cloudflare required path - direct DNS to the VPS
- Arti onion/hidden services (REQUIRED) - first-class reachability via Tor Project Rust Arti HS alongside clearnet; HS keys never in git
- Single operator-chosen VPS - size/plan open (do not invent SKUs); multi-host ends when the operator says
Primary domain: surmount.systems
Management UI: https://services.surmount.systems
Mail host: mail.surmount.systems (MX on the apex)
After compaction / context loss, read this first: docs/COMPACTION-PIN.md
Operator direction (2026-07-30): docs/operator-direction.md
Peer review of what the tree is (versions, stores; see supersession banner): docs/architecture-review.md.
Living stack map and design notes (read these before inventing a parallel design):
| Doc | Contents |
|---|---|
| docs/COMPACTION-PIN.md | Reload first after compaction; full durable direction |
| docs/operator-direction.md | Dated operator direction dump |
| docs/principles.md | Hermetic flakes, versions, security, sockets, no nginx |
| docs/glossary.md | FOD, Day-1/Day-2, Fix, Arti, DataStore, ... |
| docs/packages-and-forks.md | Surmount package overlay, Stalwart fork consume |
| docs/architecture-review.md | Versions, 0.16.15 stores, assumption review |
| docs/STACK.md | Layers, ownership, stores, identity, secrets layers |
| docs/DATASTORES.md | Data plane inventory; Stalwart storage design |
| docs/open-choices.md | Still open / proposed design notes |
| AGENTS.md | Agent process pins |
| docs/research/ | Deep / historical evidence notes |
| docs/SEARCH_AND_UI.md | JMAP search first; phases admin then webmail v1 |
| docs/EDGE_AND_TLS.md | Edge/TLS; nginx delete path; Axum-first; Arti HS required |
| docs/SECURITY.md | Threat model, FDE/LUKS2, Nostr auth notes |
| docs/SECRETS.md | Deploy secrets vs Vaultwarden vs LUKS |
| docs/OPS.md | Logs, health, scripts, self-ops |
| docs/hygiene.md | Standing engineering rules |
| docs/fix-and-fixos.md | Fix / FixOS naming and ownership ladder |
| secrets/README.md | deploy-secrets bootstrap (host-local; nothing secret committed) |
Short version: Stalwart owns mail + internal FTS (for now); our Rust layer owns admin and v1 webmail with Nostr product auth; legacy sites are static files only; traffic works direct to the VPS; deploy secrets decrypt on the host at activation (sops-nix today) and never live in the public git tree.
flake.nix # nixosConfigurations, packages, checks, devShells
modules/ # surmount.* NixOS modules
crates/management-ui/ # Axum management UI (Leptos SSR next)
nix/packages/ # Surmount package overlay (Stalwart FODs, UI)
hosts/mail-vps/ # sample host config (generic name only)
docs/ # architecture, security, secrets, migration, DNS, ops
scripts/ # operator DNS/TLS/mail smoke checks
secrets/ # docs/placeholder only (never commit secrets)
ref/ # study-only references / submodules
tests/ # NixOS VM smoke test
Read docs/hygiene.md (top rule: never secrets in git),
docs/principles.md, and AGENTS.md. Short
version: pure flake.lock builds, deploy secrets on host only for activation,
NixOS + Nix + Rust (no Python product/ops, no NPM), Maildir migration is
operator-run, Stalwart owns protocols and mail search for now, Rust owns
console/webmail and Axum-first edge, Nostr product auth, small modules,
security-first, no nginx product edge, no required Cloudflare hop, operator
direction and open choices live in docs/.
- Operator-chosen VPS. Do not invent provider names, RAM, disk, core counts, or plan SKUs in public docs (Q-HOST-1).
- Prefer a provider where you control PTR/rDNS on the sending IP.
- Prefer install paths that allow LUKS2 root (disko + nixos-anywhere); see docs/SECURITY.md and operator-direction.md.
nix develop # puts nixfmt, just, rustc, … on PATH
just dev # local management console → http://127.0.0.1:8080/
just check # CI-style host bar: fmt --check, clippy, test
just ci # full flake checks.<system>.ci (GHA runs this)
just check-ci # alias of just ci
just fmt # format check only (errors if dirty; no write)
just fmt-write # apply cargo fmt + flake nixfmt
just test # cargo test only
just clippy # clippy -D warnings
just e2e # local comprehensive end-to-end (hermetic)
nix build .#management-uijust dev runs the multi-page console with demo hostnames. No VPS or Stalwart
required (Stalwart chip stays down until something answers SURMOUNT_STALWART_URL).
Ctrl-C stops it. Override any SURMOUNT_* env before invoking.
nixfmt (Nix formatter): the flake already ships nixfmt-rfc-style as
packages.nixfmt, formatter, and in devShells.default. Prefer the flake
over a distro package so CI and laptop match.
# one-shot (no install)
nix run .#nixfmt -- --check flake.nix
# or use the formatter output:
nix run .#formatter -- flake.nix
# temporary shell with nixfmt on PATH
nix shell .#nixfmt
# durable shell with full tooling
nix develop
# optional: pin into your user profile
nix profile install .#nixfmtOn Arch, you do not need a pacman/AUR package for day-to-day work if you use the flake above. If you still want a system package for other repos:
# AUR (names vary; check with paru/yay search)
paru -S nixfmt # or nixfmt-bin / nixfmt-git, depending on AUR
# or always-nix from nixpkgs (same RFC style as this flake):
nix profile install nixpkgs#nixfmt-rfc-styleHost quality bar: just check runs format check (no write), clippy with
warnings denied, then cargo test (same order as typical local CI gates).
Full flake CI: just ci (alias just check-ci) builds checks.<system>.ci
(crane fmt/clippy/test, e2e pure lib, module-eval contracts, nixfmt, …). Heavy
mail VM / Stalwart FOD / full host toplevel stay separate (just check-heavy,
optional flake checks). Hermetic process e2e stays just e2e (not the
aggregate). Host e2e / Tor never join green CI.
GitHub Actions: .github/workflows/ci.yml
mirrors the Surmount grok-oss CI
shape (least privilege, disk reclaim, swap, flake-pinned just, retry only
cold flake eval). The quality job display name is just ci so a branch
protection required check can match that exact string (do not rename casually).
Payload is only just ci -> checks.x86_64-linux.ci.
nixos-rebuild build --flake .#mail-vps
# or
nix build .#nixosConfigurations.mail-vps.config.system.build.toplevel
# alias (same config): .#surmount-mailUse your preferred path (nixos-anywhere, manual ISO + nixos-install,
provider NixOS image). For FDE, prefer disko + nixos-anywhere with disk
encryption keys (SECURITY.md). Then:
- Copy or generate
hardware-configuration.nixintohosts/mail-vps/. - Add SSH public keys in
hosts/mail-vps/configuration.nix. - OS hostname defaults to sample
mail-vps(networking.hostName = lib.mkDefault); set the real name on the host or local overlay. - Set DNS A/AAAA for
mail,services, and apex (see docs/DNS.md). - Configure sops age keys (secrets/README.md).
- Switch:
nixos-rebuild switch --flake .#mail-vps# Always build before switch on mail hosts
nixos-rebuild build --flake .#mail-vps
nixos-rebuild switch --flake .#mail-vps
# Rollback if needed
nixos-rebuild --rollback switchAvoid nixos-rebuild switch straight from an untested main when MX is live;
build + boot into a known generation first if changes are risky.
Full checklist and sample zone: docs/DNS.md.
Minimum before real mail:
- A/AAAA for
mail.surmount.systems - MX for
surmount.systems->mail.surmount.systems - PTR matching
mail.surmount.systems - SPF + DKIM + DMARC
Maildir is the source of truth. See docs/MIGRATION.md.
# After accounts exist in Stalwart:
surmount-mail-import-maildir you@surmount.systems \
/var/lib/surmount/import/maildir/.../MaildirLegacy web sites from Synology are expected to be static files only (no app-server migration; no exceptions).
The Surmount multi-page console is the operator surface. Use Stalwart's own admin as a bootstrap fallback only (directory create, first-boot):
ssh -L 8080:127.0.0.1:8080 mail-vps
# open http://127.0.0.1:8080Transitional nginx may also expose /stalwart-admin/ on
services.surmount.systems for bootstrap; remove or lock down once you no
longer need it. Port numbers: confirm live defaults in modules (UI vs Stalwart
HTTP split is documented in architecture-review).
NixOS option path: Surmount-owned services.stalwart module for
0.16+ config.json (matches stock nixpkgs 26.05 option name; dual-disables
stock stalwart-mail.nix and stalwart.nix; Surmount still owns config.json,
not stock TOML). Unit remains stalwart-mail.service. See
modules/stalwart-service.nix and modules/mail.nix.
Multi-page Leptos SSR operator console (Axum edge + DOGE dark theme):
| Surface | Routes |
|---|---|
| HTML | /, /domains, /accounts, /system, /mail |
| JSON | /health, /api/v1/system, /api/v1/domains, /api/v1/accounts, /api/v1/stalwart/status |
| Residual | POST /api/v1/jmap (501 honest proxy boundary); full webmail UI parked |
- Package:
nix build .#management-ui - Service:
surmount-management-ui.service - Public:
https://services.surmount.systems/(product edge is Axum HTTPS whenweb.enable = false) - Optional onion display:
SURMOUNT_ONION_URLorSURMOUNT_ONION_HOSTNAME_FILE(never invent a live onion in tree) - Directory: default honest empty (
source: unavailable); hermeticmockor livestalwartonly when explicitly configured + host token (never default-on)
Honest residual: Q-AUTH-1 product answers; JMAP proxy beyond 501; v1 webmail UI; host cutover. Day-one host order: docs/OPS.md. Phases: docs/SEARCH_AND_UI.md.
./scripts/check-dns.sh
./scripts/check-tls.sh services.surmount.systems:443
./scripts/check-mail-ports.sh mail.surmount.systemsSee docs/OPS.md and scripts/README.md.
nix flake check
# or individually:
nix build .#checks.x86_64-linux.management-ui
nix build .#checks.x86_64-linux.mail-vps-eval
nix build .#checks.x86_64-linux.mail-vm-test # slowerNever secrets in git (plain or ciphertext), including LUKS unlock material. Deploy layer (host-local): secrets/README.md. Full two-bucket story (deploy secrets / Vaultwarden) plus LUKS disk encryption: docs/SECRETS.md. Hygiene: docs/hygiene.md.
How Surmount owns packages today and how to consume
SurmountSystems/stalwart without agent git:
docs/packages-and-forks.md.
Operator-owned infrastructure for Surmount Systems. Application code under
crates/ is dedicated to the public domain under the Unlicense
(SPDX: Unlicense). See UNLICENSE.md. Workspace Cargo
metadata matches (license = "Unlicense" in crates/Cargo.toml). Upstream
dependencies and vendored packages keep their own licenses (for example
Stalwart AGPL).