Architecture Overview
Vectis is a containerised, self-hosted email platform. Every component runs in a Docker container, isolated across four purpose-built networks. This page covers the container layout, data flow, database design, and security model.
Architecture diagram
Section titled “Architecture diagram” Internet | +-------------+-------------+ | | | Port 80/443 Port 25/587/465 Port 993/995 | | | +--------+--------+ | +--------+--------+ | Traefik | | | Dovecot | | (reverse proxy) | | | (IMAP/POP3) | +--------+--------+ | +--------+--------+ | | | vectis-frontend | vectis-mail | | | +--------+--------+ | +--------+--------+ | Go API | +--->| Postfix | | + Admin UI SPA | | (SMTP) | +--------+--------+ +--------+--------+ | | | vectis-data vectis-mail vectis-data vectis-orchestrator | | | +----+----+ | | | Rspamd | | +--------+--------+ | (spam/ | | | Orchestrator | | DKIM) | | | (Docker socket) | +----+----+ | +--------+--------+ | | | +----+----+ | vectis-data | ClamAV | | vectis-orchestrator| (opt.) | | | +---------+ | | | +--------+--------+ +---------+ | | Postgres | | Valkey | | | (database) | | (cache) | | +------------------+ +---------+ | | | | +------ vectis-data ----------+Container layout
Section titled “Container layout”Vectis runs 10 containers (9 if ClamAV is disabled):
| Container | Role | Networks |
|---|---|---|
| Traefik | Reverse proxy, TLS termination, ACME certificates | frontend |
| Go API | REST API + embedded admin UI (React SPA) | frontend, data, orchestrator |
| Postfix | SMTP server (inbound + outbound mail) | mail, data |
| Dovecot | IMAP/POP3 server, mail storage | mail, data |
| Rspamd | Spam filtering, DKIM signing, greylisting | mail, data |
| ClamAV | Antivirus scanning (optional) | |
| Postgres | Primary database | data |
| Valkey | Session cache, rate limiting, Rspamd data | data |
| Orchestrator | Update lifecycle, Docker socket access | orchestrator, data |
| acme.sh | Mail TLS certificate management | frontend |
The admin UI is not a separate container. The React SPA is compiled to static files and served directly by the Go API container.
Four Docker networks
Section titled “Four Docker networks”Network isolation follows the principle of least privilege. Each container connects only to the networks it needs.
| Network | Type | Purpose |
|---|---|---|
vectis-frontend | bridge (external) | Web traffic: Traefik to API and UI. The only network with external access (required for ACME challenges). |
vectis-mail | bridge (internal) | Mail service communication: Postfix, Dovecot, Rspamd, ClamAV. |
vectis-data | bridge (internal) | Database and cache access: Postgres, Valkey, and all services that query them. |
vectis-orchestrator | bridge (internal) | Orchestrator control plane: API to Orchestrator communication. |
Why this separation matters
Section titled “Why this separation matters”- Postfix and Dovecot query Postgres directly via read-only SQL users (
vectis_postfix,vectis_dovecot). This avoids routing every mail delivery through the API, which would add latency and create a single point of failure. - ClamAV is fully isolated to the mail network. It only communicates with Rspamd (for scanning) and Postfix (milter protocol). No database or API access.
- The Orchestrator is the only container with Docker socket access, mounted read-only. It controls the update lifecycle (pull images, restart services, rollback).
Port mapping
Section titled “Port mapping”Only three containers have ports mapped to the host:
| Container | Ports | Protocol |
|---|---|---|
| Traefik | 80 (HTTP), 443 (HTTPS) | TCP — TLS terminated by Traefik |
| Postfix | 25 (SMTP), 465 (SMTPS), 587 (Submission) | TCP — mail protocols handle their own TLS |
| Dovecot | 993 (IMAPS), 995 (POP3S) | TCP — implicit TLS |
Mail ports are direct-mapped (not proxied through Traefik) because mail protocols handle their own TLS via STARTTLS and implicit TLS. All inter-container communication uses Docker DNS resolution.
Data flow
Section titled “Data flow”Inbound mail
Section titled “Inbound mail”External MTA --> Port 25 --> Postfix --> Rspamd (spam check) | [ClamAV scan if enabled] | Rspamd --> Postfix | LMTP (port 24) --> Dovecot | Dovecot writes to Maildir | Postfix notifies API (internal webhook) | API fires mail.received webhook- External mail arrives at Postfix on port 25.
- Postfix queries Postgres to verify the recipient domain and mailbox exist (read-only SQL user).
- Postfix passes the message to Rspamd via the milter protocol for spam/virus scanning.
- Rspamd optionally forwards to ClamAV for antivirus scanning.
- If the message passes, Postfix delivers it to Dovecot via LMTP on port 24.
- Dovecot stores the message in the Maildir at
/var/vectis/mail/<domain>/<user>/Maildir/. - A Postfix notification script POSTs to the API’s internal endpoint.
- The API stores message metadata and fires webhook events.
Outbound mail
Section titled “Outbound mail”API (POST /send) --> Postfix (port 25) --> Rspamd (DKIM signing) | Rspamd --> Postfix | Postfix --> Remote MTA- A client sends
POST /api/v1/sendwith the message payload. - The API validates domain ownership, RBAC, and abuse detection.
- The API submits the message to Postfix via SMTP.
- Postfix passes through Rspamd, which adds the DKIM signature.
- Postfix delivers the message to the recipient’s MTA.
- The API stores message metadata and fires the
mail.sentwebhook.
API requests
Section titled “API requests”Browser/Client --> HTTPS (port 443) --> Traefik --> Go API | Postgres (read/write) Valkey (sessions, cache)- Requests arrive at Traefik on port 443 (TLS terminated).
- Traefik routes to the Go API container based on path prefix.
- The API authenticates via session cookie or API key.
- The API reads from and writes to Postgres (using
vectis_api, the full-access user). - Session lookups hit Valkey first for fast validation.
Database schema
Section titled “Database schema”Postgres is the primary data store. All tables use UUIDv7 primary keys (time-sortable, clustering-safe) generated by the Go application.
Core tables
Section titled “Core tables”| Table | Purpose | Key fields |
|---|---|---|
domains | Mail domains | name, active, dkim_selector, spam_threshold, max_mailboxes |
mailboxes | Email accounts | domain_id, local_part, password_hash (Argon2id), quota_mb |
aliases | Mail forwarding | domain_id, source_local_part, destination |
admins | Admin accounts | email, password_hash, role, totp_secret, totp_enabled |
sessions | Active sessions | admin_id, token_hash (SHA-256), ip_address, expires_at |
api_keys | API keys | admin_id, key_hash (SHA-256), key_prefix, scoped_domain_ids |
audit_log | Change history | admin_id, action, resource_type, resource_id, details (JSONB) |
messages | Message metadata | domain_id, message_id, direction, sender, recipients, status |
mail_stats | Aggregate stats | domain_id, sent, received, bounced, spam, failed |
webhooks | Webhook registrations | url, events, secret, domain_id |
orchestrator_history | Update operations | action, status, plan_summary (JSONB), snapshot_path |
backup_jobs | Async backup ops | action, status, progress, backup_path |
Three Postgres users
Section titled “Three Postgres users”Principle of least privilege applied at the database level:
| User | Access | Used by |
|---|---|---|
vectis_postfix | Read-only: domains, mailboxes, aliases | Postfix (SQL lookups for mail routing) |
vectis_dovecot | Read-only: domains, mailboxes | Dovecot (authentication + mailbox location) |
vectis_api | Full read/write on all tables | Go API server |
Postfix and Dovecot never need write access. If either container is compromised, the attacker cannot modify data.
Conventions
Section titled “Conventions”- All primary keys are UUIDv7, generated by Go (not Postgres sequences).
- All timestamps are
TIMESTAMPTZstored in UTC. - Foreign keys on domain deletion use
RESTRICT(prevents accidental cascade). - Foreign keys on admin deletion use
SET NULL(preserves audit history) orCASCADE(sessions). - Passwords use Argon2id (memory=64 MB, iterations=3, parallelism=4).
- Forward-only migrations via golang-migrate. Rollback is snapshot-based (
pg_dumpbefore apply,psqlto restore).
Security model
Section titled “Security model”Network isolation
Section titled “Network isolation”The four-network design ensures that a compromise of one service limits the blast radius:
- ClamAV compromise: attacker can only reach Rspamd and Postfix on the mail network. No database, no API.
- Admin UI compromise: the SPA communicates only via the authenticated API. No direct database access.
- Rspamd compromise: access to mail network services and Valkey (ephemeral data only). No direct API access.
Authentication and authorization
Section titled “Authentication and authorization”- Admin sessions use HMAC-signed cookies with
HttpOnly,Secure,SameSite=Strict. - Session tokens are 256-bit random, stored as SHA-256 hashes in Postgres.
- API keys use the
vectis_prefix, stored as SHA-256 hashes. Raw keys are shown only at creation. - TOTP MFA is available for admin accounts (TOTP secret encrypted at rest).
- Three RBAC roles (
super_admin,admin,domain_admin) with domain-level scoping fordomain_admin.
Service-to-service authentication
Section titled “Service-to-service authentication”- Postfix notification script authenticates to the API via an internal token (
X-Internal-Tokenheader). - Orchestrator to API communication uses mTLS (preferred) or bearer token (fallback).
- All internal communication stays on Docker internal networks (not routable from the host network).
Data protection
Section titled “Data protection”- Secrets live in
secrets.yaml(mode0600), separate from configuration. - Database credentials use three separate users with minimal privileges.
- Backup archives are encrypted with a dedicated key (or the API secret as fallback).
- DKIM private keys are stored at
/var/vectis/dkim/with restricted file permissions.
Update safety
Section titled “Update safety”- The orchestrator takes a
pg_dumpsnapshot before every update. - Health checks run after every update. If any service fails, automatic rollback restores the snapshot and reverts container images.
- Advisory locks (
pg_advisory_lock(1)) prevent concurrent update operations. - The orchestrator state machine enforces a strict sequence: plan, snapshot, migrate, pull, restart, health check.
Related
Section titled “Related”- API Overview — the REST API
- CLI Overview — managing Vectis from the command line
- CLI Commands — full command reference