Skip to content

SaaS Box Architecture

The SaaS box (public IP 62.171.156.201) runs Mezzanine as a hosted, multi-tenant product. Instead of a single hand-configured nginx like the App box, it uses Traefik for wildcard TLS and dynamic, label-driven routing, a control plane that provisions and manages tenants, and one isolated frontend/backend container pair per tenant. Everything lives on a single Docker bridge network, mezzanine-tenants.

Topology

The diagram below shows the two long-lived platform containers (Traefik and the control plane) and two example tenants. Traefik is the only container that publishes ports to the host; every other container is reachable only on the mezzanine-tenants network. Routing rules come from labels on each tenant’s frontend container, so Traefik never needs a config reload when a tenant is added or removed.

graph TD
  Internet["Internet to 80 and 443"] --> Traefik["mezzanine-traefik (traefik:v3) publishes 80 and 443"]
  subgraph net["Docker network: mezzanine-tenants (bridge)"]
    Traefik -->|"Host try.mezzanine.cloud"| CP["mezzanine-control-plane (4000)"]
    Traefik -->|"Host mya.mezzanine.cloud via label"| F1["mz-mya-frontend (3000 internal)"]
    Traefik -->|"Host satwik.mezzanine.cloud via label"| F2["mz-satwik-frontend (3000 internal)"]
    F1 -->|"api and websockets"| B1["mz-mya-backend (3001 internal)"]
    F2 -->|"api and websockets"| B2["mz-satwik-backend (3001 internal)"]
    CP -. "provisions and controls lifecycle" .-> F1
    CP -. "provisions and controls lifecycle" .-> F2
  end
  B1 --> V1[("tenant_mya_data")]
  B2 --> V2[("tenant_satwik_data")]
  CP --> VCP[("mezzanine_cp_data")]
  Traefik --> VACME[("saas_traefik_acme")]

Key points:

  • mezzanine-traefik (traefik:v3) is the only container that publishes ports to the host: 80 and 443. It terminates TLS using a wildcard certificate for *.mezzanine.cloud, obtained via the Cloudflare DNS-01 challenge (cert resolver cf) and persisted in the saas_traefik_acme volume.
  • mezzanine-control-plane (mezzanine-control-plane:latest, port 4000) is the brain of the box. It handles signup, accounts, billing (Razorpay, env-gated), the superadmin/ops console, the provisioner, and the tenant lifecycle state machine. try.mezzanine.cloud routes to it — that is the public signup site.
  • Each tenant gets a pair: mz-<slug>-frontend (mezzanine-frontend:latest, listens on 3000) and mz-<slug>-backend (mezzanine-backend:latest, the Express + better-sqlite3 + Socket.IO API on 3001). The frontend proxies /api and websockets to its own backend over the mezzanine-tenants network — neither 3000 nor 3001 is published to the host. This is exactly how the App box wires frontend to backend; the only difference is multi-tenancy is achieved by running one stack per tenant rather than one stack for everyone.
  • The current tenants are mya, satwik, and viharikakallamyaccessiocom — all currently hibernated.

Routing is driven by container labels

Traefik does not have a static vhost file. Routing for a tenant is declared as labels on that tenant’s frontend container, so the provisioner can stand up or tear down a route just by creating or removing a container. This is the exact label set the provisioner applies to each mz-<slug>-frontend:

LabelValuePurpose
traefik.enabletrueOpt this container into Traefik routing
router ruleHost rule matching &lt;slug&gt;.mezzanine.cloudMatch the tenant’s subdomain
entrypointwebsecureBind the route to the HTTPS (443) entrypoint
tls.certresolvercfIssue/renew TLS via Cloudflare DNS-01
loadbalancer.server.port3000Send traffic to the frontend’s internal port
mezzanine.tenant&lt;slug&gt;Tenant marker used by the control plane / ops tooling

The signup site is the one exception: try.mezzanine.cloud is routed by Traefik directly to the control plane on port 4000, not to any tenant pair.

Volumes

VolumeOwnerContents
mezzanine_cp_datacontrol planeControl-plane database (accounts, tenants, billing state)
saas_cp_backupscontrol planeTenant backups
saas_traefik_acmeTraefikLet’s Encrypt / ACME TLS certificates (*.mezzanine.cloud)
saas_traefik_logsTraefikTraefik access/runtime logs
tenant_<slug>_datatenant backendThat tenant’s SQLite database + uploads (one per tenant)

Each tenant’s data lives entirely in its own tenant_<slug>_data volume. This is what makes hibernation safe: stopping a tenant’s containers never touches its data.

Tenant lifecycle

The control plane drives each tenant through a state machine. Idle tenants hibernate — their containers are stopped but their data volume is kept — and wake on the next request. This keeps the box’s resource usage proportional to active tenants rather than total tenants.

stateDiagram-v2
  [*] --> provisioning: signup or ops create
  provisioning --> running: containers created and started
  running --> hibernated: idle timeout, containers stopped and volume kept
  hibernated --> running: wake on next request restarts containers
  running --> suspended: ops or billing hold
  suspended --> running: reinstated
  hibernated --> suspended: ops or billing hold
  running --> destroyed: teardown removes containers and volume
  suspended --> destroyed: teardown removes containers and volume
  destroyed --> [*]
  • provisioning — the provisioner uses the current :latest images, creates the mz-<slug>-frontend and mz-<slug>-backend containers (applying the Traefik label set above), creates the tenant_<slug>_data volume, and starts them.
  • running — both containers up; Traefik is routing <slug>.mezzanine.cloud to the frontend.
  • hibernated — containers stopped after idle, tenant_<slug>_data retained. No compute cost, data intact.
  • wake — the next request triggers a container start on the stopped pair, returning the tenant to running.
  • suspended — an ops or billing hold; the tenant is intentionally kept offline regardless of traffic.
  • destroyed — teardown: containers and the data volume are removed. Terminal.

Contrast with the App box

SaaS box (62.171.156.201)App box (5.189.154.35)
Edge / TLSmezzanine-traefik, wildcard *.mezzanine.cloud, Cloudflare DNS-01 (cf)mezzanine-nginx, certbot/Let’s Encrypt on the host
Routing configDynamic, from container labelsStatic nginx vhosts
TenancyMany isolated stacks (one pair per tenant)Single stack (MYACCESS’s own panel)
Networkmezzanine-tenantsdocker_default
Lifecycleprovision / hibernate / wake / suspend / destroyalways-on

See also