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 wrapperinternal/backend/handlers/— HTTP boundaryinternal/backend/services/— business logicinternal/backend/repositories/— SQL accessinternal/backend/db/— connection pool andBeginReadTx/BeginWriteTx(CQRS transactions)internal/backend/middleware/— auth, request limits, rate limiting, request ID, logginginternal/backend/ratelimit/— Redis-backed rate-limit policiesinternal/backend/scm/— SCM provider interfaces and event inboxinternal/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 workinternal/authz/— hot-path authorization checksinternal/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.tfinto the stack directory (OpenTofu requires a backend block in a config-dir file), and writes the credential-bearing.tfbackendconfig outside the cloned repo - runs
tofu initwith-backend-configpointed 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/...viapkg/sdk, no direct access to the database, runners, S3, or GitHub. This is the bulk ofcmd/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 sameDATABASE_URLthe 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:1234567orgoogle: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
subjectclaim 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 (
alice→alice-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 errorspkg/contracts/— backend/runner wire contracts such asRunContextinternal/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:
- Datastores & Connection Pooling — how the backend connects to PostgreSQL and Redis, with HA and read-scaling
- Configuration
- run_context_and_artifacts.md
- scm_and_pull_requests.md