Spike Curtis f5d42a868e refactor(coderd): add related-data selection to workspace queries (#28302)
This is the first of three PRs implementing lite `codersdk.Workspace`
responses ([GRU-82](https://linear.app/codercom/issue/GRU-82),
[RFC](https://app.notion.com/p/coderhq/Lite-Workspace-API-Requests-3aad579be5928060b5e7cc65c539717f)).
A fully populated workspace fans out into a large tree of DB queries
(template, latest build, provisioner job + queue position, resources,
metadata, agents, apps, statuses, scripts, log sources, template
version). Most callers do not need most of that data, and one of these
queries, `GetProvisionerJobsByIDsWithQueuePosition`, is consistently the
top resource consumer in scale tests.

This PR adds the internal plumbing to load only a selected subset of
that tree. It introduces a `workspaceRelated` selection type and threads
it through `workspaceData` and `workspaceBuildsData` down to the
individual query call sites, skipping any query whose data was not
selected.

The selection is a pointer tree that mirrors the parent/child
relationships between the objects: branch nodes are pointers (non-nil
when selected) and leaf nodes are bools. Modeling it this way makes
selecting a child without its parent unrepresentable, which matches how
loading works (a parent must be queried to learn its children's
identifiers), so there is no normalization step to forget.

No behavior change: every existing caller passes a fully populated
selection (`allWorkspaceRelated` / `allLatestBuildRelated`), so
responses are identical. Wiring the HTTP `include_related` query
parameter (Phase 2) and narrowing individual callers (Phase 3) come in
follow-up PRs.

Two notes on the current mapping:
- `queue_position` is modeled as its own node, but only one job query
exists and it always computes the ranking, so selecting `job` runs the
full query today. Splitting off a cheaper queue-position-free job query
is deferred (it needs a new SQL query + `make gen`).
- The workspace-level `LatestAppStatus` is gated on the
`latest_build.resources.agents.apps.statuses` node. It is queried
directly by workspace ID, but a caller selecting it also pulls in the
build chain as ancestors. Happy to decouple this if preferred.

<details>
<summary>Implementation plan</summary>

# Lite Workspace API — Phase 1 Plan

Add an internal configuration structure to the Coderd method that
fetches a
workspace with its related data (`workspaceData`
/`workspaceBuildsData`), thread
it down to the SQL query call sites, and skip ("squash") queries that
are not
requested. Server-internal only. No HTTP query-string parsing (Phase 2)
and no
`wsrelated` struct tags or field nil-ing (Phase 3).

## Scope

- In: config struct mapping the RFC related-data hierarchy; threading
through
`workspaceData` and `workspaceBuildsData`; gating every related-data
query;
comprehensive unit tests with a mocked `database.Store` (gomock/dbmock).
- Out: HTTP `include_related` parsing, codersdk changes, `wsrelated`
tags, docs
autogen, and changing `convertWorkspaces` skip-on-missing-data behavior.
All
existing callers pass an "include all" config, so Phase 1 is a behavior-
  preserving refactor.

## Design

New file `coderd/wsrelateddata.go` with a pointer tree that mirrors the
RFC
hierarchy. Branch nodes are pointers (present when non-nil); leaf nodes
are
bools. Because a child cannot be expressed without its parent, the RFC
"includes all ancestors" rule is a structural invariant of the type: an
illegal
selection is unrepresentable, so no normalization pass exists or is
needed.

```go
type workspaceRelated struct {
    Template    bool
    LatestBuild *latestBuildRelated
}
type latestBuildRelated struct {
    Job             *jobRelated
    Resources       *resourcesRelated
    TemplateVersion bool
}
type jobRelated struct{ QueuePosition bool }
type resourcesRelated struct {
    Metadata bool
    Agents   *agentsRelated
}
type agentsRelated struct {
    Apps       *appsRelated
    Scripts    bool
    LogSources bool
}
type appsRelated struct{ Statuses bool }
```

Helpers:

- `allWorkspaceRelated()` / `allLatestBuildRelated()` construct a fully
populated tree. Passed by all current callers so behavior is unchanged.
- Zero value (nil subtree) includes nothing but the workspace itself.
- `(*latestBuildRelated).appStatuses()` is a nil-safe accessor for the
deep
`apps.statuses` leaf so call sites descend the tree without a guard
chain.

Rationale for the pointer tree over a flat struct plus
`withAncestors()`: the
invariant holds by construction (illegal state is unrepresentable)
instead of
relying on a normalization step a caller must remember.
`workspaceBuildsData`
takes the `*latestBuildRelated` subtree directly.

### Query gating

`workspaceData(ctx, workspaces, cfg)`:

- `GetTemplatesWithFilter` gated on `Template`.
- `GetLatestWorkspaceBuildsByWorkspaceIDs` gated on `LatestBuild`; when
off, skip
  `workspaceBuildsData` entirely (empty builds).
- `GetLatestWorkspaceAppStatusesByWorkspaceIDs` (workspace-level
LatestAppStatus)
  gated on `AppStatuses`.

`workspaceBuildsData(ctx, builds, cfg)` gates each stage:

- jobs (`GetProvisionerJobsByIDsWithQueuePosition`) + eligible
provisioner
daemons gated on `Job`. Only one job query exists and it always computes
the
queue position, so `QueuePosition` does not gate a separate query yet;
it is
modeled for hierarchy/Phase 2 completeness. A cheaper job query without
the
window-function ranking is a worthwhile follow-up but is out of scope
for a
  simple Phase 1 (needs new SQL + sqlc gen).
- resources gated on `Resources`; metadata on `Metadata`; agents on
`Agents`;
apps on `Apps`; app statuses on `AppStatuses`; scripts on `Scripts`; log
  sources on `LogSources`; template versions on `TemplateVersion`.

Downstream `convertWorkspaceBuilds` already tolerates empty slices, so
squashing
queries yields zero/nil fields without extra work.

## Testing (comprehensive, mocked database.Store)

Internal tests (`coderd/wsrelateddata_internal_test.go`) build
`&API{Options: &Options{Database: dbmock.NewMockStore(ctrl)}}` and call
`workspaceData` directly. gomock is strict, so setting EXPECT only on
permitted
queries proves the rest are squashed.

Cases: include-all (every query runs); zero value (no related queries
run);
each single node (template only, latest_build only, job, resources,
metadata,
agents, apps, statuses, scripts, log_sources, template_version,
workspace-level
app statuses); the empty-resources short-circuit; and the constructors
plus the
nil-safe `appStatuses()` accessor at every depth.

## Steps

1. Add `coderd/wsrelateddata.go` (selection tree, constructors,
`appStatuses`).
2. Thread the selection through `workspaceData` and
`workspaceBuildsData`; gate
   queries by descending the tree.
3. Update the callers to pass `allWorkspaceRelated()` /
`allLatestBuildRelated()`.
4. Add unit tests; run `make fmt`, `make lint`, targeted `go test`.

## Resolved decisions

1. Pointer tree over a flat struct + normalizer, so the ancestor rule is
a
   structural invariant.
2. Cheaper queue-position-free job query deferred to a follow-up.

</details>

---

*Generated by Coder Agents on behalf of @spikecurtis.*
2026-08-20 10:40:31 +02:00
…

Coder Logo Light Coder Logo Dark

Self-Hosted Cloud Development Environments and AI Agents

Coder Banner Light Coder Banner Dark

Quickstart | Docs | Why Coder | Premium

discord release godoc Go Report Card OpenSSF Best Practices OpenSSF Scorecard license

Coder is a self-hosted platform for cloud development environments and AI coding agents. Workspaces are defined with Terraform, connected through a secure Wireguard® tunnel, and automatically shut down when not used. Coder Agents runs a native AI coding agent whose loop executes in the control plane on your infrastructure, with no API keys in workspaces.

  • Define cloud development environments in Terraform
    • EC2 VMs, Kubernetes Pods, Docker Containers, etc.
  • Automatically shutdown idle resources to save on costs
  • Onboard developers in seconds instead of days
  • Delegate coding work to AI agents on your infrastructure
    • Bring any model (Anthropic, OpenAI, Google, Bedrock, self-hosted)
    • No LLM credentials in workspaces, user identity on every action
    • Centralized model governance, cost tracking, and audit logging

Coder platform showing templates and a running workspace

Quickstart

The most convenient way to try Coder is to install it on your local machine and experiment with provisioning cloud development environments using Docker (works on Linux, macOS, and Windows).

# First, install Coder
curl -L https://coder.com/install.sh | sh

# Start the Coder server (caches data in ~/.cache/coder)
coder server

# Navigate to http://localhost:3000 to create your initial user,
# create a Docker template and provision a workspace

Install

The easiest way to install Coder is to use the install script for Linux and macOS. For Windows, use the latest ..._installer.exe file from GitHub Releases.

curl -L https://coder.com/install.sh | sh

You can run the install script with --dry-run to see the commands that will be used to install without executing them. Run the install script with --help for additional flags.

See install for additional methods.

Once installed, you can start a production deployment with a single command:

# Automatically sets up an external access URL on *.try.coder.app
coder server

# Requires a PostgreSQL instance (version 13 or higher) and external access URL
coder server --postgres-url <url> --access-url <url>

Use coder --help to get a list of flags and environment variables. See the install guides for a complete tutorial.

Documentation

Browse the documentation or visit a specific section below:

  • Workspaces: Workspaces contain the IDEs, dependencies, and configuration information needed for software development
  • Templates: Templates are written in Terraform and describe the infrastructure for workspaces
  • Coder Agents: Delegate coding work to AI agents running on your self-hosted infrastructure
  • Administration: Learn how to operate Coder
  • Premium: Learn about paid features built for large teams
  • IDEs: Connect your existing editor to a workspace

Support

Feel free to open an issue if you have questions, run into bugs, or have a feature request.

Join our Discord to provide feedback on in-progress features and chat with the community using Coder!

Integrations

New integrations are always in progress. Open an issue to request one. Contributions are welcome in any official or community repository.

Official

Community

Contributing

New contributors are always welcome. If you are new to the Coder codebase, see the contribution guide to get started.

Hiring

Apply on the careers page if you are interested in joining the team.

S
Description
Provision remote development environments via Terraform
Readme AGPL-3.0
1 GiB
Languages
Go 74.9%
TypeScript 23.1%
Shell 0.8%
HCL 0.3%
PLpgSQL 0.3%
Other 0.4%