Skip to content

Architecture Overview

A bird's-eye view of how Mailyte's email server is put together and why each piece exists. If you want to understand the system before you start building on it, this is the place.


In this section

  • Data Flow


    Follow an email from arrival to delivery — every hop, every check, every store.

    Data flow

  • Service Architecture


    Every service, its port, its dependencies, and what happens when it goes down.

    Service architecture

  • Database Design


    Tables, relationships, indexes, and the schema that holds it all together.

    Database design

  • Security Model


    Authentication, TLS, and threat protection from the architecture perspective.

    Security model

  • Scaling Strategy


    How to grow Mailyte as load increases — horizontal scaling, resource limits, bottlenecks.

    Scaling strategy

  • Organization Model


    The org/domain/mailbox hierarchy and how multi-tenancy is structured.

    Organization model

  • Multi-Tenant Isolation


    How tenant data, quotas, and configuration stay completely separated.

    Multi-tenant isolation


The Big Picture

Mailyte is a multi-tenant email server that sends, receives, filters, and stores email for multiple organizations — all from a single deployment. Think of it as running a small email hosting company inside a Docker Compose stack: roughly 45 containers in the full production stack, but organized into a handful of clear layers.

Layer What it does Key services
Edge TLS termination and per-hostname routing for all HTTP traffic (production) Traefik, acme_webroot
Mail Infrastructure The actual email traffic — SMTP in/out, spam filtering + DKIM signing, mailbox storage, certificates, log ingestion Postfix, Dovecot, Rspamd, cert_manager, log_ingestor
API Gateway + Workers The management plane (one FastAPI gateway, 34 route modules) and ~20 single-purpose worker services api, webhooks, tracking, rate_limiter, analytics, queue_manager, archiver, autoconfig, jmap, caldav, oauth, migration, and more
Security Services DLP scanning, TOTP for operator MFA, geo-blocking dlp, totp, geo_blocking
Data Stores Persistent state MySQL, Redis, Qdrant, S3 object storage (+ Kafka provisioned, unused — see below)
Observability Metrics, dashboards, alerting Prometheus, Grafana, Alertmanager, exporters
Frontends Human surfaces webmail, console (staff), Roundcube/SOGo (optional profiles), docs

Each layer only talks to the layers it needs to. Workers never handle raw SMTP. Postfix never queries Qdrant. This keeps the system modular — services can be scaled or restarted independently of mail delivery.


System Diagram

graph TB
    subgraph External
        Internet["Internet / Remote MTAs"]
        Clients["Email Clients<br>(Thunderbird, Outlook, etc.)"]
        Upstream["Upstream Systems<br>(mailyte-api / your app)"]
        Browser["Browsers<br>(webmail, console)"]
    end

    subgraph Edge["Edge (production)"]
        Traefik["Traefik<br>:80 :443"]
    end

    subgraph Mail["Mail Infrastructure"]
        Postfix["Postfix<br>SMTP :25 :587 :465"]
        Dovecot["Dovecot<br>IMAP :143 :993<br>POP3 :110 :995 · Sieve :4190"]
        Rspamd["Rspamd<br>milter :11332<br>(spam + DKIM signing)"]
        CertMgr["cert_manager"]
        LogIngestor["log_ingestor<br>(mail.log → mail_logs)"]
    end

    subgraph API["API Gateway + Workers"]
        Gateway["FastAPI Gateway (api)<br>:8080 · 34 route modules"]
        Webhooks["webhooks"]
        Tracking["tracking"]
        RateLimiter["rate_limiter"]
        Analytics["analytics"]
        QueueMgr["queue_manager"]
        StorageUsage["storage_usage"]
        Archiver["archiver"]
        RAG["rag"]
        More["autoconfig · jmap · caldav<br>oauth · migration · templates<br>url_protection · delivery_optimizer<br>encryption · activesync · dashboard<br>monitoring · dlp · totp · geo_blocking"]
    end

    subgraph Data["Data Stores"]
        MySQL[("MySQL")]
        Redis[("Redis")]
        Qdrant[("Qdrant")]
        S3[("S3 object storage")]
    end

    Internet -->|"SMTP :25"| Postfix
    Clients -->|"IMAP/POP3"| Dovecot
    Clients -->|"SMTP submit :587/:465"| Postfix
    Upstream -->|"HTTPS"| Traefik
    Browser -->|"HTTPS"| Traefik
    Traefik -->|"api.domain"| Gateway

    Postfix -->|"milter"| Rspamd
    Postfix -->|"LMTP :24"| Dovecot
    Postfix -->|"policy check"| RateLimiter
    Postfix -->|"content filter"| Tracking
    Dovecot -->|"auth + accounts"| MySQL
    Dovecot -->|"sieve archive copy"| Archiver
    LogIngestor --> MySQL
    CertMgr --> Postfix
    CertMgr --> Dovecot

    Gateway -->|"SMTP :10587"| Postfix
    Gateway --> MySQL
    Gateway --> Redis
    Gateway -->|"proxies"| Tracking
    Gateway -->|"proxies"| Analytics
    Gateway -->|"proxies"| RateLimiter
    Gateway -->|"proxies"| QueueMgr

    Tracking --> MySQL
    Webhooks --> MySQL
    Analytics --> MySQL
    RAG --> Qdrant
    Archiver --> S3
    StorageUsage -->|"IMAP QUOTA"| Dovecot
    QueueMgr -->|"shared spool"| Postfix

Service inventory

The full roster lives in Service Architecture, with per-service detail. The short version, straight from docker-compose.yml:

Group Services
Lifecycle secrets-check, migrate (Alembic), docker-proxy
Mail postfix, dovecot, rspamd, cert_manager, acme_webroot, log_ingestor
Gateway api (FastAPI, :8080, ×2 replicas in production)
Workers webhooks (×2), tracking (×2), rate_limiter, monitoring, analytics, archiver, dashboard, encryption, queue_manager, rag, storage_usage, delivery_optimizer, templates, url_protection, oauth, caldav, radicale, jmap, migration, autoconfig, activesync
Security dlp, totp, geo_blocking
Data mysql (8.0.35), redis, qdrant, kafka + zookeeper
Observability prometheus, grafana, alertmanager, mysql-exporter, redis-exporter
Frontends docs, console (profile), webmail (profile), roundcube (profile), sogo (profile)
Edge (prod) traefik

Internal vs. external ports

In production only the mail protocol ports (25, 587, 465, 143, 993, 110, 995, 4190) and Traefik's 80/443 listen on 0.0.0.0. Every other service was re-bound to 127.0.0.1 on 2026-08-22 — before that, ~30 internal services (including unauthenticated Prometheus and Qdrant) were reachable from the public internet. HTTP services are reached externally only through Traefik: api.${DOMAIN}, jmap.${DOMAIN}, caldav.${DOMAIN}, docs.${DOMAIN}, grafana.${DOMAIN}, the wildcard autoconfig.*/autodiscover.*/mta-sts.* rule, the webmail hostname, and the (IP-allowlisted) console hostname.


How the Layers Talk to Each Other

The front door. Postfix accepts email from the internet (or from clients submitting outbound mail), runs it through Rspamd for spam checks (inbound) and DKIM signing (outbound), and hands it to Dovecot over LMTP for storage. This layer speaks native email protocols — SMTP, IMAP, POP3, ManageSieve. Its integration points with the platform are precise and few: SQL lookups against MySQL, a rate-limit policy call at the SMTP DATA phase, and the tracking content filter on the submission ports.

The control plane. One FastAPI gateway (api) exposes the whole REST surface — organizations, domains, mailboxes, SMTP credentials, filters, analytics, and the webmail backend — and proxies to the single-purpose worker containers behind it (analytics, tracking, rate_limiter, queue_manager, storage_usage, monitoring). Workers run in the background: firing webhooks, recording opens and clicks, archiving mail to S3, measuring quotas over IMAP, rotating TLS certificates, running migration jobs.

MySQL is the source of truth for accounts, domains, credentials, logs, and configuration. Redis handles the fast stuff — caching, rate-limit counters, Rspamd's Bayes data. Qdrant stores vector embeddings for AI-powered search. S3 holds the encrypted mail archive and backups.


Multi-Tenancy at a Glance

Every piece of data belongs to an Organization. Organizations own Domains. Domains own Email Accounts and Aliases. This hierarchy means you can host acme.com and widgets.io under different organizations with fully isolated data, quotas, and rate limits — all in the same deployment.

graph TD
    Org["Organization<br><em>acme-corp</em>"] --> DomainA["Domain<br><em>acme.com</em>"]
    Org --> DomainB["Domain<br><em>acme.io</em>"]
    DomainA --> Mailbox1["Mailbox<br><em>alice@acme.com</em>"]
    DomainA --> Mailbox2["Mailbox<br><em>bob@acme.com</em>"]
    DomainA --> Alias1["Alias<br><em>info@acme.com → alice</em>"]
    DomainB --> Mailbox3["Mailbox<br><em>support@acme.io</em>"]
    style Org fill:#4051b5,color:#fff
    style DomainA fill:#2e7d32,color:#fff
    style DomainB fill:#2e7d32,color:#fff

Isolation is the default

Every API call is scoped to the organization that owns the credential. Cross-tenant operations require a platform-scoped credential (an operator session or an API key issued with scope='platform') — a tenant credential can never reach them, regardless of its own permission flags.

For the full breakdown, see Organization Model and Multi-Tenant Isolation.


Key design decisions

Decision Rationale
Docker Compose, not Kubernetes Keeps the barrier to entry low. A single ./start.sh gets everything running; docker-compose.prod.yml layers on Traefik, replicas, resource limits, and loopback port binding.
One gateway, many workers The api container owns the entire public REST surface and proxies to single-purpose workers. Workers can be scaled, restarted, or disabled independently without affecting mail delivery.
HTTP + Redis for inter-service communication Kafka and Zookeeper are provisioned in the stack and a client library exists (shared/kafka_client.py), but as of 2026-08-30 no service produces or consumes — the working paths are HTTP calls over the Docker network and Redis.
Postfix's spool is the outbound queue No parallel application-level mail queue to drift out of sync; queue_manager manages Postfix's own queue via postqueue over a shared volume.
Rspamd over SpamAssassin Modern, high-performance, native milter support — and it's also the DKIM signer.
Qdrant for vector search Purpose-built vector DB rather than bolting vector search onto MySQL.
Fail-closed startup secrets-check gates the entire stack on strong secrets; migrate gates schema-touching services on successful Alembic migration.
ULID primary keys All core tables use 26-char ULIDs (CHAR(26)), not auto-increment INTs — globally unique, time-sortable, safe to expose.

Topic Page
How email flows through the system Data Flow
Every service, its port, and its dependencies Service Architecture
Database tables and relationships Database Design
Authentication, TLS, and threat protection Security Model
Growing the system as load increases Scaling Strategy
The org/domain/mailbox hierarchy Organization Model
How tenant isolation works in practice Multi-Tenant Isolation

Related sections: