Skip to content

Architecture overview

Where code lives, what depends on what, and where a change belongs

Concourse is a pnpm and Turborepo workspace. apps/ holds executable workspaces, including the independent operator CLI. packages/ holds things that are shared.

Read this page to find where a change belongs. Read Module shape for the rules once you are there.

Source validation uses combined checks. Turbo retains compiler passes where generated files or alternate configurations require them.

Deployables

Each deployable app has a vercel.json. The Applications page links each app guide.

WorkspaceWhat it serves
apps/concourseThe application and the system administration interface
apps/docs.concourseThis documentation site
apps/docs.tredisThe TREDIS 6 User Manual
apps/midotMichigan Department of Transportation tools
apps/us-countiesA map for selecting U.S. counties

The dependency graph

The graph shows the runtime dependencies in each workspace package.json, and the entry points that each workspace imports. It leaves out development-only edges, such as the tooling packages @concourse/oxlint-config, @concourse/typescript-config, and @concourse/vitest-config. They carry no runtime meaning.

  apps/concourse      ──> api, sdk, services, auth, db, rsdb/runtime, contracts, email,
                          platform, ui, tailwind-config
  apps/cli            ──> sdk, auth, db, rsdb/local-generation, contracts, email, platform
  apps/midot          ──> api/midot, sdk/midot, services/rsdb/mi-property-value,
                          services/rsdb/tredis-global-dynamic, rsdb/mi-property-value,
                          rsdb/tredis-global-dynamic, rsdb/runtime, contracts, email
  apps/us-counties    ──> ui, tailwind-config
  apps/docs.concourse ──> docs, ui
  apps/docs.tredis    ──> docs, ui

  api       ──> services, auth, contracts
  services  ──> db, rsdb/mi-property-value, rsdb/tredis-global-dynamic, contracts, email
  auth      ──> db, email, platform, contracts
  db        ──> contracts, platform
  rsdb      ──> contracts
  sdk       ──> contracts
  docs      ──> ui

  contracts, email, platform, ui, and tailwind-config depend on no workspace.

Two things to notice.

The graph is layered and acyclic. Nothing imports upward, and .dependency-cruiser.ts enforces that through pnpm lint:boundaries. contracts is the browser-safe leaf that every layer shares. It imports no other workspace. services owns shared Project and Application operations. Isolated Services entry points can also own behavior for a registered RSDB database. api exposes shared operations and Auth's remaining domain operations over HTTP. sdk uses the shared contracts.

The web app uses the SDK for organizations, members, invitations, SSO configuration, account security, administration, Projects, and Applications. Rackspace verification uses app-local server actions. Server calls use the API handler in the same process. Identity flows and page-access gates stay on Auth. See ADR 0005.

rsdb depends only on contracts. It imports no other workspace, even though db owns stored verification reports and Services owns database-specific behavior. That is the single most confusing thing about this repository, so it has its own section below.

Ports and adapters

The workspace uses ports and adapters, also called Hexagonal architecture. ADR 0017 defines the terms and the dependency rule: source dependencies point inward. This table gives the role of each workspace.

RoleWorkspaces
Coreservices, and the domain modules of auth: access decisions, policy, and sessions
Shared kernelcontracts: browser-safe schemas and the HttpApi declarations
Driving adaptersapi, sdk, apps/*/app/, apps/*/lib/**/framework/, the MIDOT server routes, and apps/cli
Driven adaptersdb, rsdb, email, platform, and the Better Auth adapter of auth
Composition rootsapps/concourse/lib/runtime.ts, apps/midot/lib/runtime.ts, and the CLI runtime modules
Outside the modelui, docs, tailwind-config, the tooling packages, apps/us-counties, and the two documentation sites

@concourse/db/repositories holds the application repositories. rsdb holds the typed sessions and the repositories for each Rackspace database, one entry point per database. A core module asks for a repository port, and the composition root provides its Drizzle adapter.

Current exceptions

None. pnpm lint:boundaries enforces the dependency rule, and no storage code is outside a driven adapter. Each MIDOT sign-in route that changes state makes one MidotSignIn call. GET /api/session makes no core call: it only decodes the session cookie, as the session middleware does for every protected route.

Rackspace storage and access

packages/db, packages/rsdb, and database-specific Services answer different questions. The two database packages define types whose names start with Rackspace, but they share no symbol.

QuestionOwner
What did we record about a Rackspace schema and its verification report?packages/db
What is a Rackspace schema, and does the deployed database still match it?packages/rsdb
What does a property value project operation mean?packages/services

packages/db owns application storage: one report row and the audit events for each verification run. packages/rsdb owns MySQL access, the committed schema snapshots, the registry, and what a verification result means. Its private factory turns reviewed database bindings into concrete typed capabilities; it does not publish a generic application SQL reader. Database-specific Services can own legacy application behavior while RSDB retains generated schemas, Drizzle sessions, the repository queries, per-database transactions, shared pool lifecycles, and cleanup. These external writes do not use the SQLite audit path. A transaction does not span RSDB databases.

A third body of Rackspace code lives in apps/concourse/lib/rackspace/, and it belongs there. It orchestrates a run: the report row in the application database, audit rows under the administrator's identity, and the admin pages. The browser advances through the immutable registry snapshot one database at a time and saves each result before continuing. A failed result stops the run. The application stores no step ledger or lease. rsdb stays independent of application persistence and orchestration. The Application context records the decision.

Effect backend composition uses one model across backend packages. Domain operations compose Effects, repositories hide Drizzle, and application Layers own configuration and scoped resources. Promise execution stays at framework and vendor boundaries. React and Next.js code keeps ordinary TypeScript control flow. Package ownership remains unchanged. The shared HTTP declaration drives API handlers and both SDK entries. @concourse/sdk returns Promise methods; @concourse/sdk/effect serves Effect programs. The Effect toolchain decision scopes its rules to Effect-owned code. Mixed apps put framework adapters in framework/ folders. Effect-owned modules use Effect's platform services. Reviewed Node-only operations live in .node.ts host adapters with static node: imports. @concourse/platform defines the shared HMAC boundary. Effect supplies the other portable platform services. Executable roots choose Node adapters; domain Layers stay open to replacement.

Shared contracts

Browser-safe schemas and types live in @concourse/contracts, the leaf that db, auth, and the app depend on. Persistence derives its column enums from them. Auth owns identity and organization permissions. Services own Project and Application permissions and transitions. @concourse/contracts/property-value owns the canonical legacy project shape, while its Services entry point owns behavior. The verification result schema also lives here, so rsdb and the app read results with the same rules. See ADR 0004.

@concourse/contracts/rackspace combines database results with execution identity and deployment policy. It uses the verification invariants of the same entry point. Historical report validation remains independent of the current Registry.

Where does my change go?

ChangeDestination
A page, a route, a form, a Server Actionapps/concourse/app/
Application Layer composition (repositories, adapters, domain Layers)apps/concourse/lib/runtime.ts
Shared server runtime lifetime and the request SDK clientapps/concourse/lib/server/runtime.ts and lib/server/framework/client.ts
Request-bound reads and navigation refusalsDomain framework/reads.ts modules and lib/reads/framework/execution.ts
Browser identity clientapps/concourse/lib/framework/client.ts
Application write completion and affected-view refreshapps/concourse/lib/applications/framework/actions.ts
A shared application table, relation, or migrationpackages/db. It owns the shared application schema. See ADR 0001.
Identity, sessions, organization accesspackages/auth. It owns access decisions. See ADR 0002.
A Project or Application operationpackages/services. See ADR 0005.
Legacy behavior for a registered RSDB databaseAn isolated packages/services entry backed by typed packages/rsdb access. See ADR 0011.
An HTTP route of the APIpackages/api, one file per resource under lib/routes/
How another program calls the APIpackages/sdk
A schema that crosses a package boundarypackages/contracts. See ADR 0004.
A shared technical capability missing from Effectpackages/platform; runtime adapters belong in its host entry point.
A shared componentpackages/ui
Shared documentation UIpackages/docs
A documentation pagedocs/concourse/
A rule that applies everywheredocs/concourse/development/standards/
Michigan Digital Guidelines componentsDirect upstream imports in apps/midot

If a change needs a new package, read when a package is justified first. The answer is usually no.

The server entry point creates one database connection and composes Auth, Services, and the API handler. It creates one SDK client per render with the incoming credentials and audit request facts. Domain read modules cache protected reads only within that render. lib/reads/framework/execution.ts maps Auth and API refusals. The browser entry point exposes Better Auth for identity. Browser domain interactions use Server Actions that call the server SDK client. These composition modules do not own domain permissions.

Application catalog and memberships

Persistence stores the application catalog and organization memberships. Services own their reads, administrator writes, and effective modes. The API serves these operations. Concourse presents catalog management and organization app lists on the Apps tab. Application PRD records the model. Midot enforcement remains outside this implementation.

The app's Application write module calls the SDK and refreshes affected views after success. The shared form-action helper returns its final Notice. Form adapters parse input and select complete operations. They do not reconstruct refresh paths. This keeps completion behavior in one place without moving permissions, audit writes, or lifecycle decisions out of Services and Persistence. The action tests cover refresh and Notice outcomes. The browser lifecycle covers catalog status, Saved mode, and Effective mode.

Projects

Persistence owns Projects and their explicit memberships. Services combine organization roles, grants, and archive status to decide Project access, built on the roles Auth owns. Organization landing pages show accessible Projects. System administrators use a separate support entry without broadening ordinary organization access.

PRD-0003 defines shared identity and access behavior. The Persistence guide owns tenant-safe grant storage. The Services guide owns support access and query scoping.

The ADR index lists the decisions behind this structure. The Rackspace security page owns the shared-account risk. The performance standard owns cache and feedback rules. The MIDOT guide owns its upstream Michigan component and Node function boundaries. Each context keeps its own vocabulary in CONTEXT-MAP.md.

Operator runtime

apps/cli owns database maintenance, bootstrap, local Maildev, Rackspace generation, SDK examples, and deployment tooling. The Concourse app owns Production build migrations; the CLI checks the resulting deployment. See ADR 0013. Each command family composes only the public package capabilities it needs. The CLI and applications own independent environment files. Their dependencies connect code and Turbo task hashes, not dotenv values. See runtime-local configuration.

Verification ownership

The command reference records explicit integration selection and cache ownership. Browser setup prepares fixtures. The verification graph selects the database suites separately.