Skip to content
GantryCD

Architecture

GantryCD has four runtime surfaces and one shared source-of-truth layer:

  • Backend: Go HTTP API plus scheduled jobs
  • Runner group: long-lived Go worker coordinator
  • Ephemeral runner: short-lived Go process launched per run
  • CLI: gantrycli (cmd/cli/) — mostly an HTTP client like Web, plus a direct-DB break-glass subtree
  • Web: React SPA

PostgreSQL is the source of truth for application state. The backend also talks to S3-compatible storage, AWS STS, Redis, and GitHub.

Components

Backend

The backend is wired in the cmd/backend package. It owns:

  • HTTP routing and middleware
  • session auth for users
  • JWT auth for runner groups and ephemeral runners
  • deployment orchestration and queue promotion
  • credential generation for state, logs, SCM, and cloud runtimes
  • leader-elected background jobs such as runner cleanup and deployment status reconciliation
  • the async task queue’s worker slots, which run continuously on every replica and are not leader-elected (see async_tasks.md)

Major packages:

  • internal/backend/server/ — router and server wrapper
  • internal/backend/handlers/ — HTTP boundary
  • internal/backend/services/ — business logic
  • internal/backend/repositories/ — SQL access
  • internal/backend/db/ — connection pool and BeginReadTx/BeginWriteTx (CQRS transactions)
  • internal/backend/middleware/ — auth, request limits, rate limiting, request ID, logging
  • internal/backend/ratelimit/ — Redis-backed rate-limit policies
  • internal/backend/scm/ — SCM provider interfaces and event inbox
  • internal/backend/notify/ — notification provider interfaces (Slack); deliveries are an async outbox (see notifications.md)
  • internal/backend/sso/ — SSO providers (GitHub and Google static singletons, OIDC built lazily per callback from per-org rows; issuer-keyed discovery cache)
  • internal/backend/jobs/ — scheduled background work
  • internal/authz/ — hot-path authorization checks
  • internal/clients/aws/, internal/clients/github/ — outbound client wrappers

Runner Group

The runner group is a long-lived process that polls for work and launches per-run workers.

Implemented launcher variants:

  • local process launcher: cmd/runner/group/local
  • Docker launcher: cmd/runner/group/docker
  • GitHub Actions launcher: cmd/runner/group/github

Shared coordination logic lives in:

  • internal/runner/group/

The runner group owns:

  • periodic poll loop
  • periodic heartbeat loop
  • launch concurrency limits
  • ephemeral runner JWT generation
  • child-process discovery and shutdown

Ephemeral Runner

The ephemeral runner is a real worker process, not a stub. Its entrypoint is cmd/runner/ephemeral/main.go; execution logic lives in internal/runner/ephemeral/.

It:

  • signals ready
  • accepts the run and receives RunContext
  • clones the repo at the requested commit
  • when GantryCD manages the backend: writes a single run-ID-named backend "s3" {} override .tf into the stack directory (OpenTofu requires a backend block in a config-dir file), and writes the credential-bearing .tfbackend config outside the cloned repo
  • runs tofu init with -backend-config pointed at the out-of-repo config file
  • executes the mode-specific OpenTofu command
  • uploads plan artifacts and log chunks
  • polls status to keep liveness fresh and detect cancellation
  • reports completion and deregisters

Web

The web app lives under web/src. It is a pure backend client:

  • all reads and writes go through /api/v1/...
  • no direct access to the database, runners, S3, or GitHub

CLI (gantrycli)

cmd/cli/ is two surfaces in one binary:

  • The resource commands (deployments, stacks, orgs, runner-groups, pats, cloud-integrations, queue, auth, profile, whoami, ping, local-plan, …) are a pure backend client, same as Web — all reads and writes go through /api/v1/... via pkg/sdk, no direct access to the database, runners, S3, or GitHub. This is the bulk of cmd/cli/.
  • cmd/cli/operator/ is a separate, smaller break-glass subtree (e.g. super-admin promotion, first-org bootstrap). It connects directly to PostgreSQL with the same DATABASE_URL the backend uses, bypassing HTTP authz entirely; it is not part of the request path and is only safe to run with database credentials.

User Identity

GantryCD has no concept of a “local” user. Every user account is provisioned at SSO login time, and the primary key users.id is derived deterministically from the IdP-supplied triple:

user_id = "<provider_prefix>:<idp_username>:<idp_subject>"
  • For single-instance providers (GitHub, Google) the prefix is the static kind, e.g. github:alice:1234567 or google:alice@acme.com:108412345678901234567.
  • For multi-instance providers (OIDC) the prefix is oidc-<12-hex SHA-256 of issuer URL>, e.g. oidc-a3f2d1b8c4e5:bob:sub-uuid. Different self-hosted IdPs therefore cannot collide on user_id.
  • The encoding helpers live in pkg/domain/userid.go (BuildUserID, ParseUserID, OIDCProviderKey, UserIDProviderPrefix).

Consequences:

  • Recycling defense. When an IdP reuses a freed username for a different person, the new subject claim yields a new user_id that does NOT inherit the original account’s orgs, role assignments, or audit history. This is the one invariant the encoding actively defends.
  • No rename concept. An IdP-side login change (alicealice-jones) produces a different user_id and is treated as a different user. There is no rebind path; permissions are picked up fresh via group entitlements on the next login, the same way any other user gets access.
  • No rename surface. Neither Backstage nor the operator CLI can change a username — the column is a denormalized cache of the IdP login claim.

The very first super-admin on a new install is created out-of-band: sign in once via SSO so the user row exists, then run gantrycli operator users promote-super-admin --user-id=<id> from a host with database access. Subsequent super-admins are promoted from the backstage UI.

Communication Boundaries

These boundaries are architectural constraints:

  • Web → Backend only
  • Runner group / ephemeral runner → Backend only
  • Backend → PostgreSQL, Redis, S3-compatible storage, AWS STS, GitHub

Runners never talk to the web app or to each other.

Shared Packages

  • pkg/domain/ — domain entities, validations, and typed errors
  • pkg/contracts/ — backend/runner wire contracts such as RunContext
  • internal/contextutil/, internal/logutil/, internal/otel/ — shared internal utilities

Storage And Integrations

  • PostgreSQL stores all orchestration state.
  • S3-compatible storage holds Terraform/OpenTofu state, run logs, and work artifacts.
  • AWS STS mints temporary credentials for state access, log uploads, and cloud integrations.
  • Redis backs rate limiting and pull-request refresh caching when configured.
  • GitHub is used for SCM integration discovery, commit syncing, and pull-request refresh.

See also: