Local Development
Fusion's local development experience is built on .NET Aspire: an AppHost, written in C# or TypeScript, starts your subgraphs and composes them into a running gateway. Without it, every change to a type, a field, or a resolver means re-exporting the schema, re-composing with the Nitro CLI, and restarting the gateway.
The HotChocolate.Fusion.Aspire package integrates composition into the Aspire AppHost. When you run the AppHost, the orchestrator starts your subgraphs, fetches their source schemas from live endpoints, composes them into a Fusion archive, and writes it to the gateway project directory. This startup flow replaces the manual export-compose-restart cycle. Mark each local subgraph with WithGraphQLHttpEndpoint() so composition uses the schema exposed by the running service. The examples on this page show both AppHost languages.
If different teams own subgraphs in separate repositories, bind each team's AppHost to a shared Nitro stage. The AppHost composes the live schemas from the team's local subgraphs with the stage's published fusion configuration. That configuration supplies the schemas and routes for the remaining subgraphs. You can then develop and debug your team's subgraphs inside the full graph without copying another team's schema artifacts into your repository. See Developing Across Teams and Repositories.
Prerequisites#
You need an Aspire AppHost project with the HotChocolate.Fusion.Aspire package.
If you do not have an AppHost yet, create it with:
dotnet new aspire-apphost -n AppHostAdd the Fusion Aspire package to the AppHost project:
cd AppHost
dotnet add package HotChocolate.Fusion.AspireEach subgraph project needs a schema-settings.json file in its project directory so the orchestrator can identify the source schema and choose how to fetch it. Generate this file with the schema export command from HotChocolate.AspNetCore.CommandLine. If you followed Getting Started, you already have it.
Setting Up the AppHost#
The AppHost wires together your subgraphs and gateway. Three extension methods configure the composition pipeline.
var builder = DistributedApplication.CreateBuilder(args);
builder.AddNitroComposition();
var productsApi = builder
.AddProject<Projects.Products>("products-api")
.WithGraphQLHttpEndpoint();
var reviewsApi = builder
.AddProject<Projects.Reviews>("reviews-api")
.WithGraphQLHttpEndpoint();
builder
.AddProject<Projects.Gateway>("gateway-api")
.WithNitroComposition()
.WithReference(productsApi)
.WithReference(reviewsApi);
builder.Build().Run();Four things to notice:
AddNitroComposition()registers the composition orchestrator with the Aspire eventing system. Call this once on the application builder.WithGraphQLHttpEndpoint()declares the GraphQL route of a subgraph and the path its source schema is downloaded from. The orchestrator waits for the subgraph to start, then fetches the source schema over HTTP.WithNitroComposition()marks the gateway as needing composition. The orchestrator discovers all referenced subgraphs, extracts their schemas, composes them, and writes agateway.farfile to the gateway project directory.WithReference()is standard Aspire. It tells the orchestrator which subgraphs to include in composition for this gateway.
In a TypeScript AppHost, the same extension methods surface camelCased, optional parameters are gathered into a single options object, and every chained call is awaited.
When you build and run the AppHost, the orchestrator handles the entire composition pipeline automatically. No manual nitro fusion compose step needed.
Live Schema Extraction#
WithGraphQLHttpEndpoint() declares two paths: path is the GraphQL route the subgraph serves (/graphql by default), and schemaPath is where the schema document is downloaded from (/graphql/schema.graphql by default). An Apollo Federation subgraph serves its schema through the GraphQL endpoint at path via _service.sdl, so schemaPath is ignored for it. The orchestrator reads schema-settings.json to tell the two kinds of subgraphs apart.
The orchestrator starts each subgraph, waits for it to become healthy, then fetches its schema. If the subgraph is not ready within the timeout, the orchestrator reports an error and the dependent gateway fails to start. Other resources continue running.
You can customize the GraphQL route, the schema download path, and the endpoint name. You can also provide the expected source schema name to validate the fetched schema:
var productsApi = builder
.AddProject<Projects.Products>("products-api")
.WithGraphQLHttpEndpoint(
path: "/graphql",
schemaPath: "/graphql/schema.graphql",
endpointName: "http",
sourceSchemaName: "Products");Every parameter has a default, so pass only what differs. The source schema name comes from schema-settings.json. When you pass sourceSchemaName, it acts as an assertion and must match that configured name exactly. It does not rename the source schema, so you can usually omit it.
Developing Across Teams and Repositories#
In an organization where teams own subgraphs in separate repositories, each team can keep an AppHost alongside the subgraphs it develops. Register each local subgraph with WithGraphQLHttpEndpoint() so the AppHost fetches its live schema. Bind the AppHost to the graph's shared Nitro stage to compose those local schemas with the published fusion configuration. Nitro supplies the remaining source schemas and routes, so the gateway still represents the full graph while only your team's subgraphs run on your machine.
Sign in once with the Nitro CLI (installation). The AppHost reads the session that the CLI stores and never signs in on its own:
nitro loginThen bind the AppHost to a stage and tell the gateway which Nitro API carries its fusion configuration:
var builder = DistributedApplication.CreateBuilder(args);
builder.AddNitroComposition("dev");
var productsApi = builder
.AddProject<Projects.Products>("products-api")
.WithGraphQLHttpEndpoint();
builder
.AddProject<Projects.Gateway>("gateway-api")
.WithNitroApiId("QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==")
.WithNitroComposition()
.WithReference(productsApi);
builder.Build().Run();AddNitroComposition()registers the orchestrator for composing only the subgraphs that run locally.AddNitroComposition(stage)additionally binds the distributed application to one stage in Nitro. Calling both is safe and registers the orchestrator once. CallingAddNitroComposition(stage)twice with different stage names throws during AppHost configuration, because a run composes against a single stage.WithNitroApiId(apiId)selects the Nitro API whose fusion configuration a gateway composes against. The API id is the id that the Nitro dashboard and the Nitro CLI report for the API, the same value that--api-idtakes. Calling the method again replaces the previously configured id. On a resource that is not composed, the API id is accepted as identity metadata for your own tooling and the pull and compose flow ignores it. If you set an API id without callingAddNitroComposition, the resource console tells you that it cannot take effect.
What Happens When a Gateway Starts#
Before the orchestrator starts a gateway process that carries an API id:
- The fusion configuration for that API id and the
AddNitroCompositionstage is downloaded from Nitro. The download runs while the orchestrator waits for the local subgraphs to become healthy, so the two waits do not add up. - Each source schema of the distributed application replaces the source schema of the same name in the downloaded configuration. A source schema whose name does not appear there is added to the composition.
- Each source schema that the distributed application runs is reached at the allocated HTTP endpoint of its Aspire resource. The orchestrator combines that endpoint with the path of the
urlin the subgraph'sschema-settings.json, or with/graphqlwhen the settings define no usable path. - Each source schema that only the downloaded configuration carries is external. The gateway reaches it at its
devUrl, or at itsurlwhen nodevUrlis defined. Composition logs a warning for every external source schema without adevUrl, because a deployed URL is often not reachable from a developer machine. A subgraph that runs in the local AppHost but has no allocated HTTP endpoint at composition time cannot receive an injected URL either, so it is treated like an external schema for URL resolution, which is why such a resource can also trigger the missing-devUrl warning. Seetransports.http.devUrl. - The composed archive is written to the gateway project directory as usual. The gateway console then reports which fusion configuration it composed against, when that configuration was downloaded, and which external source schemas it carries together with the URLs they resolved to.
The downloaded configuration is the only base the composition builds on. What a previous composition wrote to gateway.far is never an input again, so a source schema that was removed or renamed upstream also disappears from your next run.
Variable substitution follows the same split as the URL resolution. The settings of the subgraphs you run resolve against the Aspire environment in schema-settings.json, while the settings that the downloaded configuration carries resolve against the stage name you passed to AddNitroComposition.
A composition or download failure fails only the gateway it belongs to. The rest of the distributed application keeps running.
Matching Source Schemas by Name#
Replacement matches source schemas by name. WithGraphQLHttpEndpoint() uses the name from the subgraph's schema-settings.json. If you pass sourceSchemaName, it must match that name exactly or composition fails. It does not rename the source schema.
A local source schema whose configured name does not match the one in Nitro is added next to it instead of replacing it, which usually surfaces as a composition error about a conflicting field. Make sure the name in schema-settings.json matches the published source schema name.
Rebuilding a Subgraph#
A rebuild or restart of a local subgraph recomposes the gateway schema exactly as it does without Nitro, and the recomposition reuses the fusion configuration that was downloaded when the gateway started. A run therefore downloads once per gateway, and your inner loop stays as fast as a local-only composition. To pick up what was published to the stage in the meantime, use the gateway's Recompose command described below, or restart the AppHost.
When a gateway never acquired a configuration because it failed to start, later composition attempts are skipped with a log entry instead of composing against a stale base.
You do not need to stop the run to restart one subgraph. While aspire run keeps the AppHost in the foreground, restart the subgraph from a second terminal with the Aspire CLI:
aspire resource products-api restartThe command executes against the running AppHost. The orchestrator sees the restart, recomposes the schema of every gateway that references the subgraph, and logs the recomposition on the gateway console. When a recomposition fails, the gateway keeps the previous schema. The dashboard offers the same action as Restart in the resource submenu on the Resources page.
The gateway additionally exposes a Recompose command. It downloads a fresh fusion configuration first, so it also picks up what was published to the stage since the run started, without an AppHost restart. Invoke it from the resource submenu of the gateway or from the CLI:
aspire resource gateway-api recomposeWorking Offline#
Every downloaded fusion configuration is cached per Nitro API URL, API id, and stage, next to the Nitro CLI configuration (~/.config/nitro/cache/fusion on macOS and Linux, %APPDATA%\nitro\cache\fusion on Windows). The cache lives outside your repository, so it survives a clean and is shared across working trees.
When the download fails for any reason, including no network, a rejected or expired sign-in, and a stage without a fusion configuration, the gateway composes against the cached copy. Its console gets a warning that the configuration could not be refreshed, names the timestamp of the cached copy, and tells you to run nitro login when the sign-in expired. Only when there is no cached copy at all does the gateway fail to start with that reason as its error.
Continuous Integration and Self-Hosted Nitro#
Two environment variables configure the AppHost's connection to Nitro. They carry the same names and the same meaning as in the Nitro CLI, so a shell that is set up for the CLI also configures the AppHost.
| Variable | Purpose |
|---|---|
NITRO_API_KEY | Authenticates with an API key instead of an interactive session. It takes precedence over the CLI session file. Set it where nitro login cannot run, for example in CI. |
NITRO_CLOUD_URL | Points the AppHost at a self-hosted Nitro instance. Without it, the AppHost uses the API URL that nitro login stored, and https://api.chillicream.com when there is none. |
Composition Settings#
Composition settings are configured in Nitro. Open the gateway document and go to Settings > Schema Registry > Composition to control how source schemas are merged when composing the gateway's configuration.

The page carries toggles for global object identification and for removing unreferenced definitions, merge behavior selections for the @cacheControl and @tag directives, and a tag list that excludes tagged definitions from the composite schema.
Settings apply at publish time. The banner on the page states that changes take effect from the next publish onward, and only when publishing with the nitro-fusion-publish GitHub Action, the NitroFusionPublish Azure DevOps task, or the nitro fusion publish CLI command. A stage keeps the fusion configuration it already carries until that next publish, so changing a setting does not alter what a bound AppHost downloads in the meantime.
The output file name defaults to gateway.far. You can change it if needed:
builder
.AddProject<Projects.Gateway>("gateway-api")
.WithNitroComposition(outputFileName: "composed.far")
.WithReference(productsApi)
.WithReference(reviewsApi);How Composition Fits the Dev Loop#
With Aspire, your inner dev loop looks like this:
- Change code in a subgraph (add a field, modify a type, adjust a resolver).
- Build and run the AppHost.
- The orchestrator starts your subgraphs, extracts their schemas, and composes the Fusion archive.
- The gateway loads the new archive and exposes the updated composite schema.
- Open Nitro at the gateway endpoint and query immediately.
With AddNitroComposition, step 3 composes on top of the fusion configuration that was downloaded when the gateway started, so the loop stays the same while your subgraph runs inside the full graph.
If composition fails (for example, a field conflict or a missing lookup), the orchestrator logs the error on the gateway console and the gateway fails to start. Every other resource keeps running, so you can fix the issue and restart the gateway, which composes again. You get the same composition validation as the Nitro CLI, integrated into your build step.
Validating Changes Against the Stage#
When AddNitroComposition binds the run to a stage and the gateway selects a Nitro API with WithNitroApiId, every composition is followed by a validation of the composed gateway schema against that stage. The AppHost uploads the schema to Nitro, and Nitro compares it with the schema version and the clients registered for the API and stage:
- Schema changes are classified as breaking, dangerous, or safe.
- Every registered client is checked with its persisted operations. A violated operation is reported with its hash, its deployed tags, and the message, code, path, and location of each error.
Validation is observational. The composed archive is installed before validation starts, so a finding never blocks the gateway, and the gateway serves the newly composed schema either way. Findings surface in two places:
- The gateway console logs the findings, grouped by client and operation.
- The Aspire dashboard shows a notification that names the gateway and the stage and links to the console logs. When a later composition passes again, a follow-up notification reports the recovery. A run that never violates anything triggers no notifications.
A composition that yields an unchanged gateway schema is not validated again, so a restart without schema changes sends no request to Nitro. Validation runs only while the AppHost runs; aspire publish never validates.
To turn validation off for a gateway:
builder
.AddProject<Projects.Gateway>("gateway-api")
.WithNitroComposition(disableValidation: true);Disabling validation turns off only the schema upload and the reports. Composition, the configuration download, and the archive install are unaffected.
Next Steps#
- Need to compose without Aspire? See the Nitro CLI composition workflow in Adding a Subgraph.
- Need entity resolution patterns? See Entities and Lookups for public vs. internal lookups, composite keys, and the node pattern.
- Need cross-subgraph field dependencies? See Data Requirements for
@requireand FieldSelectionMap patterns. - Need visibility controls? See Schema Exposure and Evolution for
@inaccessible,@internal,@deprecated,@requiresOptIn, and@override. - Driving the run from the terminal?
aspire runandaspire resourcein the Aspire CLI reference apply to C# and TypeScript AppHosts alike.