Skip to content

Deployment

The platform runs on Scaleway (containers, PostgreSQL, object storage, secrets, VMs) and Cloudflare (Pages, Workers, DNS). Infrastructure is OpenTofu in infra/terraform; each service repo's .gitlab-ci.yml builds and deploys it.

Environments: shared (cross-environment resources), dev, stg, prod.

Topology

graph TB
    Browser[Browser]

    subgraph "Cloudflare"
        Portal[Portal<br/>Cloudflare Pages]
        Realtime[Realtime Worker<br/>+ GatewayRoom DO]
    end

    subgraph "Scaleway, per environment"
        Nexus[nexus container]
        Authz[authz container]
        Bucket[(OTA release bucket)]
        Relays[Relay VMs]
    end

    subgraph "Scaleway, shared"
        RDB[(Shared RDB<br/>one database per env)]
        Zitadel[Zitadel container<br/>auth.digilan-edge.com]
        ZDB[(Zitadel RDB)]
    end

    Agent[Edge agent<br/>OpenBSD]

    Browser --> Portal
    Portal --> Nexus
    Portal <--> Realtime
    Portal --> Zitadel

    Agent --> Nexus
    Agent <--> Realtime
    Agent --> Bucket
    Agent <--> Relays

    Nexus --> RDB
    Nexus --> Authz
    Nexus --> Zitadel
    Nexus --> Bucket
    Authz --> RDB
    Realtime <--> Nexus
    Relays --> Nexus
    Zitadel --> ZDB

What runs where

Runtime What Source
Scaleway Serverless Containers, namespace digilan-<env> nexus-<env>, authz-<env>; radius-api-<env> and Monsieur Wifi where enabled modules/environment
Scaleway Serverless Container (shared) Zitadel environments/shared/zitadel.tf
Scaleway Managed PostgreSQL digilan-shared (PostgreSQL 17) One database per environment (db_name), used by nexus and authz; radius-<env> and mrwifi-<env> where enabled environments/shared/database.tf, modules/database
Scaleway Managed PostgreSQL zitadel (PostgreSQL 16) Zitadel's database environments/shared/zitadel.tf
Scaleway Container Registry rg.fr-par.scw.cloud/digilan nexus, authz, zitadel, radius-api, radius-freeradius images environments/shared/main.tf
Scaleway Object Storage digilan-agent-releases-<env>, private modules/environment/ota.tf
Scaleway Instances Relays (relay_count), FreeRADIUS VM (where enabled) modules/relay, modules/environment
Scaleway Secret Manager JWT keypair, release publisher key, OTA signing key modules/environment, environments/shared/ota-signing.tf
Scaleway Transactional Email Zitadel mail environments/shared/email.tf
Cloudflare Pages digilan-portal-<env> (and digilan-radius-<env>) modules/dns
Cloudflare Workers digilan-realtime-{dev,stg,production} platform/realtime/wrangler.toml
Cloudflare DNS, zone digilan-edge.com portal, nexus, authz, auth CNAMEs; relay host records; realtime custom domain modules/dns, modules/relay, wrangler.toml

All serverless containers default to min_scale = 0, so the first request after an idle period pays a cold start.

Hostnames

Service dev stg prod
Portal dev.portal.digilan-edge.com stg.portal.digilan-edge.com portal.digilan-edge.com
Nexus dev.nexus.digilan-edge.com stg.nexus.digilan-edge.com prod.nexus.digilan-edge.com
Authz dev.authz.digilan-edge.com stg.authz.digilan-edge.com authz.digilan-edge.com
Realtime dev.realtime.digilan-edge.com stg.realtime.digilan-edge.com realtime.digilan-edge.com
Zitadel auth.digilan-edge.com (shared)

Prod nexus keeps the prod. prefix; the other prod services do not.

How services are released

Component Trigger dev stg / prod
nexus git tag builds :dev, :stg, :prd images; deploy_dev runs automatically deploy_stg / deploy_prd manual
authz git tag builds all three images; deploy_dev manual manual
portal git tag deploy_dev automatic deploy_stg / deploy_prd manual
realtime git tag deploy_dev automatic deploy_stg / deploy_prd manual
edge agent git tag publishes to beta in dev and prod promote:stable:<env>:<arch> manual (dev, prod)
radius-ctrl git tag API and SPA deploy to dev not deployed

Merging to main builds and deploys nothing in any of these repos; a merge request runs checks only. Details: Nexus, Portal, Agent releases.

Standing up an environment

  1. Shared first. environments/shared creates the registry, the shared RDB, Zitadel and its database, mail, the OTA signing key and the nexus service user. Zitadel's SMTP, login policy and service user need zitadel_pat (an instance-level admin PAT) and a second apply.
  2. Environment tfvars. Fill environments/<env>/terraform.tfvars from tfvars.example, including the shared_rdb_* values from the shared outputs, zitadel_url, zitadel_pat and nexus_zitadel_service_token.
  3. Apply the environment. tofu apply in environments/<env> creates the database, container namespace, containers, OTA bucket and keys, Pages project, DNS and Zitadel applications. Containers stay in error until their images exist.
  4. Images. Tag platform/nexus and platform/authz, then run their deploy jobs for the environment.
  5. Realtime secret. Set the Worker's INTERNAL_API_KEY from the internal_api_key output (see Terraform → Secrets), then run the realtime deploy_<env> job.
  6. Portal. Set the portal CI variables <ENV>_ZITADEL_CLIENT_ID and <ENV>_ZITADEL_PROJECT_ID from the portal_client_id and zitadel_project_id outputs, then run the portal deploy_<env> job.
  7. OTA. Set the edge-gateway CI variables listed in Agent releases and tag the agent.
  8. First admin. Insert the first PLATFORM_ADMIN grant (Authz); everything after that is done from the portal.
  9. Relays (dev and prod). Set relay_count, relay_zone_suffix and relayd_package_url (token as TF_VAR_relayd_package_token) and apply. See Relay Fleet.

Per-component pages