feat(router): request selected supergraphs - #1284
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the router's schema state management to support request-specific supergraph variants, allowing plugins to select different supergraphs dynamically (e.g., for feature flags or schema overrides). It introduces a bounded FIFO cache for supergraph runtimes, moves validation, normalization, and planning caches to be supergraph-specific, and adds a background cleanup task for retired supergraphs. Feedback focuses on resolving a performance bottleneck in the feature flags plugin where a Mutex lock is held over heavy operations, replacing several .unwrap() calls on Mutex locks with .expect() to provide explanatory panic messages, and flattening deeply nested control flows in both the cleanup task and subscription chunk loops.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR introduces request-scoped supergraph selection via plugins, splitting schema-derived state (public Supergraph + SupergraphSnapshot) from router-owned runtime state (RouterSupergraphRuntime). It adds a new supergraph.source: plugin mode (no configured loader) and ensures the selected supergraph is used consistently across validation, normalization, planning, execution, introspection, usage reporting, deduplication, and WebSockets.
Changes:
- Add public
Supergraphownership + snapshot/retirement model in the executor crate, and allow plugins to select it viaOnHttpRequestHookPayload::set_supergraph. - Refactor the router to build and cache per-supergraph
RouterSupergraphRuntime(configured runtime eagerly, plugin-selected runtimes lazily via a bounded FIFO cache + cleanup task). - Update probes (readiness/health), WebSockets, and e2e tests to respect request-pinned supergraph selection and per-runtime cache isolation; add new plugin examples (
replace_schema) and updatefeature_flagsto plugin-only sourcing.
Reviewed changes
Copilot reviewed 40 out of 42 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| plugin_examples/replace_schema/supergraph.graphql | Adds example supergraph SDL used by the replace-schema plugin example. |
| plugin_examples/replace_schema/src/test.rs | Adds e2e tests verifying per-request supergraph override and fail-closed behavior. |
| plugin_examples/replace_schema/src/plugin.rs | Implements a plugin that constructs and selects an alternate Arc<Supergraph> per request. |
| plugin_examples/replace_schema/src/main.rs | Adds a runnable example binary registering the plugin. |
| plugin_examples/replace_schema/src/lib.rs | Exposes plugin and tests for the new example crate. |
| plugin_examples/replace_schema/router.config.yaml | Adds example router config using supergraph.source: file plus the replace-schema plugin. |
| plugin_examples/replace_schema/Cargo.toml | Adds the new replace-schema-plugin-example crate. |
| plugin_examples/feature_flags/src/test.rs | Expands tests for plugin-only supergraph selection and runtime build failure/no-selection paths. |
| plugin_examples/feature_flags/src/plugin.rs | Migrates feature_flags example to select Arc<Supergraph> in on_http_request and cache variants in-plugin. |
| plugin_examples/feature_flags/router.config.yaml | Switches the example to supergraph.source: plugin. |
| plugin_examples/Cargo.toml | Registers the new replace_schema example crate in the plugin examples workspace. |
| plugin_examples/Cargo.lock | Updates lockfile for added example crate and dependency graph changes. |
| lib/router-config/src/supergraph.rs | Adds SupergraphSource::Plugin config variant and display name mapping. |
| lib/executor/src/plugins/hooks/on_supergraph_load.rs | Refactors supergraph loading output into public Supergraph + snapshots + retirement token model. |
| lib/executor/src/plugins/hooks/on_http_request.rs | Adds OnHttpRequestHookPayload::set_supergraph for plugin selection into request extensions. |
| lib/executor/src/plugins/hooks/on_graphql_validation.rs | Removes with_schema and clarifies validation schema semantics. |
| lib/executor/Cargo.toml | Adds tokio-util dependency for CancellationToken. |
| e2e/src/supergraph.rs | Updates e2e to validate cache isolation across configured supergraph reloads (per-runtime caches). |
| Cargo.lock | Updates workspace lockfile for new tokio-util usage. |
| bin/router/src/supergraph/mod.rs | Returns a specific error for supergraph.source: plugin (no loader). |
| bin/router/src/supergraph/base.rs | Adds NoLoaderForPluginSource load error variant. |
| bin/router/src/shared_state.rs | Removes shared schema caches and keeps parse cache local to shared state. |
| bin/router/src/schema_state.rs | Major refactor: adds RouterSupergraphRuntime, selection/pinning, bounded runtime cache, and cleanup task. |
| bin/router/src/plugins/plugins_service.rs | Runs plugins for health/readiness but continues to bypass coprocessor; keeps Prometheus bypass. |
| bin/router/src/pipeline/websocket_server.rs | Pins/validates supergraph on first WS message; rejects operations if no supergraph or retired selection. |
| bin/router/src/pipeline/validation/mod.rs | Moves validation cache access to per-runtime cache keyed by selected supergraph. |
| bin/router/src/pipeline/query_plan.rs | Moves plan cache access to per-runtime cache and uses snapshot planner for planning. |
| bin/router/src/pipeline/normalize.rs | Refactors normalization to use snapshot + runtime caches instead of a shared schema cache. |
| bin/router/src/pipeline/mod.rs | Uses request-selected supergraph throughout pipeline; updates dedupe fingerprint to use supergraph instance id. |
| bin/router/src/pipeline/execution.rs | Switches execution to use selected runtime executors and snapshot metadata/schema. |
| bin/router/src/pipeline/error.rs | Adds pipeline error variant for supergraph runtime build errors. |
| bin/router/src/pipeline/demand_control/runtime.rs | Updates demand-control evaluation to use SupergraphSnapshot. |
| bin/router/src/pipeline/coerce_variables.rs | Updates variable coercion to use SupergraphSnapshot. |
| bin/router/src/pipeline/authorization/mod.rs | Re-exports AuthorizationMetadataExt and adjusts imports after refactor. |
| bin/router/src/lib.rs | Wires cache-size observers to new shared/runtime cache layout and ensures response coprocessor uses pinned selection. |
| bin/router/src/http_utils/probes.rs | Makes readiness request-aware (SchemaState::is_ready(&req)) so plugin selection can satisfy readiness. |
| bin/router/src/cache_state.rs | Updates cache size observers to sum per-runtime caches and read parse cache from shared state. |
| .changeset/supergraph_source_plugin.md | Changeset documenting supergraph.source: plugin. |
| .changeset/replace_the_schema_state_in_the_on_http_request_plugin_hook.md | Changeset documenting set_supergraph + runtime split/caching behavior. |
| .changeset/health_readiness_plugin_hooks.md | Changeset documenting plugin hook behavior for health/readiness. |
| .changeset/drop_with_schema_from_on_graphql_validation_plugin_hook.md | Changeset documenting removal of with_schema and migration guidance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
🐋 This PR was built and pushed to the following Docker images: Image Names: Platforms: Image Tags: Docker metadata{
"buildx.build.provenance/linux/amd64": {
"builder": {
"id": "https://github.com/graphql-hive/router/actions/runs/29856788774/attempts/1"
},
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/docker/dockerfile@1.22",
"digest": {
"sha256": "4a43a54dd1fedceb30ba47e76cfcf2b47304f4161c0caeac2db1c61804ea3c91"
}
},
{
"uri": "pkg:docker/gcr.io/distroless/cc-debian12@latest?platform=linux%2Famd64",
"digest": {
"sha256": "e8e7ee4b8b106d4c5fde9e422a321b2b8a2d5cca546c97adcce927f3e1d36e36"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "router.Dockerfile"
},
"parameters": {
"frontend": "gateway.v0",
"args": {
"cmdline": "docker/dockerfile:1.22",
"label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
"label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"label:org.opencontainers.image.licenses": "MIT",
"label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.title": "router",
"label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.vendor": "theguild",
"label:org.opencontainers.image.version": "pr-1284",
"source": "docker/dockerfile:1.22"
},
"locals": [
{
"name": "context"
},
{
"name": "dockerfile"
}
],
"root": {
"configSource": {
"path": "router.Dockerfile"
},
"request": {
"args": {
"label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
"label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"label:org.opencontainers.image.licenses": "MIT",
"label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.title": "router",
"label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.vendor": "theguild",
"label:org.opencontainers.image.version": "pr-1284",
"vcs:localdir:context": ".",
"vcs:localdir:dockerfile": "docker",
"vcs:revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"vcs:source": "https://github.com/graphql-hive/router"
}
}
},
"compatibilityVersion": 30
},
"environment": {
"github_actor": "enisdenjo",
"github_actor_id": "11807600",
"github_event_name": "pull_request",
"github_event_payload": {
"action": "synchronize",
"after": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
"before": "d1d3845513e7b07401a41f74d256700c85f5988f",
"enterprise": {
"avatar_url": "https://avatars.githubusercontent.com/b/187753?v=4",
"created_at": "2024-07-02T08:52:28Z",
"description": "",
"html_url": "https://github.com/enterprises/the-guild",
"id": 187753,
"name": "The Guild",
"node_id": "E_kgDOAALdaQ",
"slug": "the-guild",
"updated_at": "2026-07-11T07:16:45Z",
"website_url": "https://the-guild.dev/"
},
"number": 1284,
"organization": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"description": "Schema registry, analytics and gateway for GraphQL federation and other GraphQL APIs.",
"events_url": "https://api.github.com/orgs/graphql-hive/events",
"hooks_url": "https://api.github.com/orgs/graphql-hive/hooks",
"id": 182742256,
"issues_url": "https://api.github.com/orgs/graphql-hive/issues",
"login": "graphql-hive",
"members_url": "https://api.github.com/orgs/graphql-hive/members{/member}",
"node_id": "O_kgDOCuRs8A",
"public_members_url": "https://api.github.com/orgs/graphql-hive/public_members{/member}",
"repos_url": "https://api.github.com/orgs/graphql-hive/repos",
"url": "https://api.github.com/orgs/graphql-hive"
},
"pull_request": {
"_links": {
"comments": {
"href": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments"
},
"commits": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits"
},
"html": {
"href": "https://github.com/graphql-hive/router/pull/1284"
},
"issue": {
"href": "https://api.github.com/repos/graphql-hive/router/issues/1284"
},
"review_comment": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}"
},
"review_comments": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments"
},
"self": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/1284"
},
"statuses": {
"href": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343"
}
},
"active_lock_reason": null,
"additions": 2435,
"assignee": null,
"assignees": [],
"author_association": "MEMBER",
"auto_merge": null,
"base": {
"label": "graphql-hive:main",
"ref": "main",
"repo": {
"allow_auto_merge": false,
"allow_forking": true,
"allow_merge_commit": false,
"allow_rebase_merge": false,
"allow_squash_merge": true,
"allow_update_branch": true,
"archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
"blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
"clone_url": "https://github.com/graphql-hive/router.git",
"collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
"commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
"compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
"contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
"created_at": "2024-11-20T16:16:12Z",
"default_branch": "main",
"delete_branch_on_merge": true,
"deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
"description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
"events_url": "https://api.github.com/repos/graphql-hive/router/events",
"fork": false,
"forks": 16,
"forks_count": 16,
"forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
"full_name": "graphql-hive/router",
"git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
"git_url": "git://github.com/graphql-hive/router.git",
"has_discussions": false,
"has_downloads": false,
"has_issues": true,
"has_pages": false,
"has_projects": false,
"has_pull_requests": true,
"has_wiki": false,
"homepage": "https://the-guild.dev/graphql/hive/docs/router",
"hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
"html_url": "https://github.com/graphql-hive/router",
"id": 891604244,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
"issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
"keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
"labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"merge_commit_message": "PR_TITLE",
"merge_commit_title": "MERGE_MESSAGE",
"merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
"milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
"mirror_url": null,
"name": "router",
"node_id": "R_kgDONSTNFA",
"notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
"open_issues": 62,
"open_issues_count": 62,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
},
"private": false,
"pull_request_creation_policy": "all",
"pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
"pushed_at": "2026-07-21T18:20:18Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 10502,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 94,
"stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
"subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
"svn_url": "https://github.com/graphql-hive/router",
"tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
"teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
"topics": [
"apollo-federation",
"federation",
"federation-gateway",
"graphql",
"graphql-federation",
"router"
],
"trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
"updated_at": "2026-07-21T08:44:41Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 94,
"watchers_count": 94,
"web_commit_signoff_required": false
},
"sha": "a822fe117fb035d300bed69f28b140bf57efb446",
"user": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
}
},
"body": "Supersedes and therefore closes graphql-hive/router#1269, closes graphql-hive/router#1137, addresses graphql-hive/router#524 through a plugin\n\nAllow a plugin to select a supergraph document during on_http_request and have it hold for each HTTP request or WebSocket upgrade. The selected supergraph is used consistently across validation, introspection, normalization, planning, authorization, demand control, execution, coprocessors, usage reporting, subscriptions, and request deduplication.\n\nThe implementation separates two kinds of state that were previously bundled together:\n\n* `Supergraph` (executor) owns schema-derived data that a plugin can safely construct and retain\n* `RouterSupergraphRuntime` (router) owns router-specific state derived from both that supergraph and the router configuration\n\nConfigured supergraphs still build their runtime eagerly before publication. Plugin-selected supergraphs build their runtime lazily on first use and reuse it through a bounded router-owned cache.\n\nAlso adding `supergraph.source: plugin`, where there is deliberately no configured fallback. A plugin must select a supergraph for each request that needs one.\n\n# Executor (public)\n\n## `Supergraph`\n\n`Supergraph` is the public owner handle. Plugins construct it with either:\n\n```rust\nSupergraph::from_sdl(...)\nSupergraph::from_document(...)\n```\n\nConstruction only accepts query-planner options. It does not accept router configuration, telemetry, callback subscription maps, HTTP clients, or other router-owned state.\n\nThe schema-derived data includes:\n\n* The parsed supergraph document\n* The public consumer schema and its SDL\n* The query planner and planner supergraph state\n* Schema metadata\n* A process-local cache identity\n\nPlugins retain an `Arc<Supergraph>` for as long as that variant should remain selectable. `OnHttpRequestHookPayload::set_supergraph` immediately takes a `SupergraphSnapshot` and stores that snapshot in request extensions. The router does not retain the plugin's `Arc<Supergraph>` owner.\n\nThis distinction is important. A request snapshot keeps the immutable schema data alive, but it does not keep the owner alive. The plugin remains the authority over whether new requests can select a variant!\n\n## Retirement\n\n`Supergraph` owns a `CancellationToken`. `SupergraphSnapshot` receives a clone of that token. When the last `Arc<Supergraph>` is dropped, `Supergraph::drop` cancels the token.\n\nThis gives retirement the required ownership semantics without callbacks from the executor crate into the router:\n\n1. The plugin removes or replaces an `Arc<Supergraph>` from its own state\n2. New requests can no longer obtain that owner\n3. Dropping the final owner publishes retirement\n4. Ordinary requests that already have a snapshot can finish\n5. Active subscriptions observe retirement and close with the schema-reload error\n6. The router's background cleanup task removes the retired runtime from its cache\n\nA snapshot or runtime never delays owner retirement. Conversely, retiring the owner does not invalidate immutable schema data already held by an ordinary in-flight request (they can finish).\n\n# Router (internal)\n\n## `RouterSupergraphRuntime`\n\n`RouterSupergraphRuntime` contains state that cannot be built by a plugin because it depends on router configuration or router-owned infrastructure:\n\n* The subgraph executor map\n* Operation-name forwarding configuration\n* Authorization metadata used by the router pipeline\n* Validation cache\n* Normalization cache\n* Query-plan cache\n* Demand-control runtime and formula cache\n\nRuntime construction receives a `SupergraphSnapshot`, the router configuration, telemetry, and the shared callback subscriptions map. It retains neither the `Supergraph` owner nor the snapshot. Requests and streams carry the snapshot separately.\n\nThis split keeps the public `Supergraph` portable and prevents plugins from having to construct fake `HiveRouterConfig`, `TelemetryContext`, callback maps, or router state.\n\n## Why caches live on the runtime\n\nMy original design shared schema-aware caches keyed by a supergraph cache ID. The final implementation instead places every schema-derived cache directly on its `RouterSupergraphRuntime`.\n\nThis is simpler and gives the same isolation guarantee structurally:\n\n* Two distinct `Supergraph` instances cannot share validation, normalization, plan, or demand-control formula entries because they have different runtimes\n* Cache keys dont need a supergraph ID because the cache itself belongs to exactly one runtime\n* Retiring or evicting a runtime naturally retires its cache entries with it\n* Configured schema rotation does not globally invalidate unrelated plugin caches\n* Old configured caches remain available only to requests or streams still holding the old runtime\n\nNote that the parse cache remains shared because parsing a GraphQL operation is schema-independent!\n\nThe process-local supergraph ID is still required for two purposes:\n\n* Looking up a plugin-selected runtime in the runtime cache\n* Partitioning in-flight request deduplication so identical operations against distinct supergraph instances never join the same execution\n\nIt's important to know that the ID is instance identity, not content identity! Two separately constructed supergraphs receive different IDs even if their SDL or consumer schemas are identical.\n\n# Configured and plugin-selected runtimes\n\n## Configured supergraph\n\n`SchemaState` has one atomic configured slot containing:\n\n* The configured `Arc<Supergraph>` owner\n* Its `SupergraphSnapshot`\n* Its eagerly built `Arc<RouterSupergraphRuntime>`\n\nThese values are published together with `ArcSwap`. A request can therefore never observe a schema from one generation and a runtime from another.\n\nThe configured runtime does not participate in the bounded plugin runtime cache. The current configured value is pinned by the configured slot and is never evicted. It follows the same ownership and retirement rules as a plugin variant, but its owner is retained by `SchemaState` instead of a plugin.\n\nConfigured loading and rotation follow this order:\n\n1. Load and parse the new supergraph SDL\n2. Run the existing supergraph load hooks\n3. Construct the schema-only `Supergraph`\n4. Run end hooks that may replace it\n5. Build `RouterSupergraphRuntime`\n6. Atomically publish owner, snapshot, and runtime\n7. Drop the previous configured owner after the swap\n\nIf schema or runtime construction fails, the current configured value remains untouched. A successful swap retires only the previous configured generation.\n\nSwapping no longer clears the complete shared schema caches or globally closes all subscriptions because dropping the old supergraph wil retire it and that will clean up its own cache and related subscriptions.\n\n## Plugin-selected supergraph\n\nPlugin runtimes are built lazily because the router cannot know which variants a plugin will select. `SchemaState` uses a `Mutex<VecDeque<...>>` as a strict FIFO runtime cache:\n\n* Maximum size is 10\n* The same supergraph ID reuses the same runtime\n* Cache hits do not change insertion order\n* Runtime construction is serialized under the mutex, so concurrent first requests build once\n* Failed construction is not inserted\n* Selecting a broken plugin variant fails closed and never falls back to the configured supergraph\n* Inserting an eleventh live variant evicts the oldest cached runtime\n\nFIFO eviction only removes the router's cached `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, publish retirement, or close subscriptions. If the plugin selects that still-live supergraph again, the router will rebuild its runtime.\n\n# Background runtime cleanup\n\nA router-managed `RuntimeCacheCleanupTask` receives registrations when plugin runtimes enter the cache. Each registration includes the supergraph ID and a clone of its retirement token. The task waits for retirement and removes the matching runtime entry.\n\nFIFO eviction sends a corresponding eviction message so the task can cancel and discard a waiter that no longer has a cached runtime to clean up. Registration is deduplicated by supergraph ID, including the case where a still-live supergraph is evicted and later inserted again.\n\nThis task has deliberately narrow authority:\n\n* Owner retirement can remove a runtime cache entry\n* FIFO eviction can remove a runtime cache entry\n* Runtime removal cannot retire a `Supergraph`\n* Runtime removal cannot stop an ordinary request or stream that already holds its own runtime `Arc`\n\nThe cache is still bounded if the cleanup task is unavailable. The task exists to release retired executors and schema caches earlier, not to provide the memory bound.\n\nConfigured runtimes do not need cleanup-task registration. Replacing the atomic configured slot immediately releases the slot's old runtime reference therefore dropping it. Requests and streams keep it alive only for as long as they still use it.\n\n## Eviction != retirement\n\nRuntime eviction and supergraph retirement are deliberately separate lifecycle events.\n\nThe plugin runtime cache stores only:\n\n```\nsupergraph ID -> Arc<RouterSupergraphRuntime>\n```\n\nIt does not store the owning `Arc<Supergraph>` or a complete `SelectedSupergraph`.\n\nEviction removes only the cache's `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, cancel the supergraph's retirement token, make the supergraph unselectable, or close its subscriptions.\n\nA running request or subscription holds its own SelectedSupergraph, which contains:\n\n* The `SupergraphSnapshot`\n* An `Arc<RouterSupergraphRuntime>`\n\nTherefore, evicting the cached runtime does not invalidate work already using it. The subscription's runtime remains alive through its own Arc, and the subscription continues normally.\n\nIf the plugin still owns the Supergraph and selects it again after its runtime was evicted, the router builds and caches a new runtime. Both runtimes are derived from the same immutable supergraph and router configuration, so this temporary overlap is safe.\n\nSubscriptions close with `SUBSCRIPTION_SCHEMA_RELOAD` **only** when the selected supergraph retires. Retirement occurs when the final owning Arc is dropped. `Supergraph::drop` cancels its retirement token, and subscription producers observing that token broadcast the reload error and stop. Existing subscriptions may continue using the previous runtime while new requests use the rebuilt runtime.\n\nThe background cleanup task may remove a cached runtime after its owner retires, but that removal is a consequence of retirement, not its cause. The retirement token closes subscriptions; cache cleanup only releases the router's cached runtime reference.\n\n# Request selection and consistency\n\n`SchemaState::select_supergraph` applies one selection rule:\n\n1. Reuse a `SelectedSupergraph` already pinned to the request (selecting multiple times throught)\n2. Otherwise prefer a plugin-provided `SupergraphSnapshot` from request extensions (plugin set)\n3. Otherwise use the configured owner, snapshot, and runtime (configuration set)\n4. If neither exists, return no selection\n5. If a plugin runtime cannot be built, return the runtime error without falling back\n * Deliberate decision, if a plugin provided a supergraph but cant be built - it would be a security issue to fall back to the configured one...\n\n`SelectedSupergraph` contains the exact snapshot and runtime pair. It is stored back into request extensions for both plugin and configured selections. This makes later users consume the same generation rather than reading the global configured slot again.\n\nThe selected pair is used by:\n\n* Validation and validation plugins\n* Progressive override state\n* GraphQL request and analysis coprocessors\n* Normalization\n* Variable coercion\n* Authorization\n* Query planning and query-plan plugins\n* Demand control\n* Introspection\n* Subgraph execution\n* Operation-name forwarding\n* Usage reporting\n* In-flight request deduplication\n* The GraphQL response coprocessor\n\nThe response coprocessor reads the pinned request selection. This matters during configured rotation because re-reading the current configured slot after execution could expose SDL from a newer generation than the one that produced the response.\n\n# Ordinary requests, deduplication, and subscriptions\n\n## Ordinary requests\n\nAn ordinary request keeps `SelectedSupergraph` alive through execution and response processing. If the owner retires during the request, the snapshot and runtime remain valid and the request completes normally.\n\nThis avoids aborting safe immutable work while still preventing future requests from selecting a removed owner.\n\n## In-flight request deduplication\n\nThe request fingerprint now includes the selected supergraph's instance ID instead of the consumer-schema checksum. Consumer schemas can be identical while routing supergraphs, subgraph endpoints, planner state, or ownership lifetimes differ.\n\nThis prevents requests for different supergraph instances from sharing an in-flight execution. Deduplicated subscription consumers still share one producer only when they selected the same supergraph instance and have the same remaining fingerprint inputs.\n\n## Subscription retirement\n\nThe subscription producer pump retains the complete `SelectedSupergraph`, not only its token. This keeps the selected snapshot and router runtime alive for the full stream lifetime, including after the HTTP handler has returned streaming headers.\n\nThe pump selects between the next upstream item and `SupergraphSnapshot::retired()`. Retirement broadcasts the existing error and stops the producer like before during schema reload.\n\nEach closes only subscriptions selected from the retired owner. The previous global `close_all_with_error` call on configured reload is removed.\n\n## WebSockets\n\nA valid WebSocket upgrade resolves and pins its `SelectedSupergraph` during the first message (not during the HTTP upgrade).\n\nAn upgrade without any selected or configured supergraph rejecting with HTTP 503 would be invisible to browsers because browsers dont provide insights to the WebSocket client about why the HTTP Upgrade failed.\n\nWe therefore instead accept the WebSocket connection and close it with a specific close code and message surfacing the error to the clients and giving insight into what happened.\n\nIf that supergraph later retires:\n\n* Existing subscription producers terminate through the retirement token\n* New operations on the connection are rejected as unavailable (explained above)\n* The connection cannot silently switch to a newly configured generation\n\nThis is the same lifetime rule as an ordinary request, extended to the WebSocket connection instead of one operation.\n\n# `supergraph.source: plugin`\n\nThe new configuration is:\n\n```yaml\nsupergraph:\n source: plugin\n```\n\nThis mode means there is no configured supergraph:\n\n* No file, Hive, or storage loader is created\n* No supergraph polling task is registered\n* The configured slot remains empty\n* Plugins, telemetry, caches, callback handling, and the HTTP server still initialize normally\n* A GraphQL request without a plugin-selected supergraph returns `NO_SUPERGRAPH_AVAILABLE` with HTTP 503\n* A WebSocket connection without a plugin-selected supergraph is closed with `No supergraph available yet`\n* A selected supergraph whose runtime cannot be built returns an internal runtime error\n\nExisting environment overrides for file and Hive sources continue to use the existing configuration override behavior. No plugin-source environment variable is added.\n\n# Health, readiness, and Prometheus\n\nHealth and readiness now pass through the plugin `on_http_request` chain and its `on_end` callbacks. This is required because readiness in plugin-only mode must allow the plugin to select a supergraph for that specific readiness request.\n\nProbe behavior is:\n\n* Health reports process liveness and remains 200 unless a plugin explicitly changes or ends the request\n* Readiness resolves the request's plugin-selected supergraph first and the configured fallback second\n* Readiness is 200 only when the selected supergraph is not retired and has a usable runtime\n* Missing selection or runtime construction failure produces 503\n* Coprocessors do not run for health or readiness\n* Prometheus keeps bypassing both plugins and coprocessors\n\nReadiness selection is request-local. A plugin selecting a supergraph for one readiness request does not publish it as a configured default.\n\n# Callback subscriptions\n\nAll configured and plugin-selected runtimes receive the same router-owned callback subscriptions map. The router also runs one heartbeat enforcer over that map.\n\nPlugins never construct or own this infrastructure. Sharing the map preserves callback routing and heartbeat enforcement regardless of which supergraph a request selected.\n\n# Hook and error ownership\n\nSupergraph load hooks continue to apply only to configured-source loading. They receive schema state without router-only executor state, and a plugin-selected `Supergraph` does not trigger configured reload hooks.\n\nError ownership follows the schema/runtime split:\n\n* SDL parsing and planner construction errors come from `Supergraph` construction\n* Subgraph executor and authorization runtime construction errors come from `RouterSupergraphRuntime` construction\n* Missing selection uses `PipelineError::NoSupergraphAvailable`\n* A plugin runtime build failure uses `PipelineError::RouterSupergraphRuntimeError` and is not cached\n\n`OnGraphQLValidationStartHookPayload::with_schema` is removed because it cannot provide pipeline-wide consistency. Plugins should migrate to `set_supergraph` in `on_http_request`.\n\n# Examples\n\n## `plugin_examples/replace_schema`\n\nThis example has a normal configured file supergraph. The plugin builds and retains one additional `Arc<Supergraph>` during initialization.\n\nRequests without the `x-schema-variant: basic` header use the configured default. Requests with that header select the plugin's stripped `basic` supergraph. The example demonstrates request-local override with a configured fallback, including validation and introspection behavior.\n\nIt also covers the fail-closed rule: selecting a variant whose router runtime cannot be constructed returns a runtime error instead of falling back to the configured schema.\n\n## `plugin_examples/feature_flags`\n\nThis example uses:\n\n```yaml\nsupergraph:\n source: plugin\n```\n\nThere is no configured default. The plugin owns the base document and a map of `Arc<Supergraph>` variants. It normalizes the feature-flag header, constructs each variant once, retains it in the map, and selects it with `set_supergraph`.\n\nEvery GraphQL request and readiness request must receive a plugin selection. If the plugin intentionally skips selection, the router returns `NO_SUPERGRAPH_AVAILABLE`. The example demonstrates full plugin ownership and switching among multiple schemas without any configured fallback.\n\nTogether the examples cover both supported models: override a configured default, or make the plugin the only supergraph source.\n\n# Intentional tradeoffs\n\n* Runtime construction is serialized on cache misses. Misses should be rare, and this guarantees one construction without another single-flight abstraction\n* The plugin runtime cache uses strict FIFO with a fixed capacity of 10. Hits do not refresh order, keeping behavior deterministic\n* Failed runtime construction is retried on a later request. Negative caching is deferred until repeated deterministic failures are shown to be a real problem\n* Retirement is based on owner lifetime, not SDL equality. Rebuilding identical SDL creates a new lifecycle and a new deduplication identity\n* Ordinary in-flight requests are allowed to finish after retirement. Only long-lived subscription work is actively terminated\n* The configured runtime is not placed in the plugin FIFO cache. Its atomic slot already provides the required permanent pin and rotation boundary\n\n# TODOs\n\n- [ ] docs\n- [ ] ~~rename ~~`~~SchemaState~~`~~ because its confusing, or merge it with ~~`~~RouterSharedState~~` for later times\n- [X] rename `new_supergraph_data` in `OnSupergraphLoadEndHookPayload` to just `new_supergraph` because it points to `Supergraph`. or should it point to `SupergraphData` still?\n- [X] (we shouldnt) should we expose the actual supergraph runtime error? I think it should be exposed unless masking is disabled (future)\n- [ ] ~~subgraph url overrides should have sort of a label that will allow users to override with dynamic supergraphs~~ think and decide about labeling in the future. for now, when the plugins are the only one that can dhynamically switch the supergraph - they have full control by managing their own map of the supergraphs, i.e. subgraph url overrides can happen directly in the plugin",
"changed_files": 43,
"closed_at": null,
"comments": 3,
"comments_url": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments",
"commits": 42,
"commits_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits",
"created_at": "2026-07-15T22:43:28Z",
"deletions": 623,
"diff_url": "https://github.com/graphql-hive/router/pull/1284.diff",
"draft": false,
"head": {
"label": "graphql-hive:super-replace-schemastate",
"ref": "super-replace-schemastate",
"repo": {
"allow_auto_merge": false,
"allow_forking": true,
"allow_merge_commit": false,
"allow_rebase_merge": false,
"allow_squash_merge": true,
"allow_update_branch": true,
"archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
"blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
"clone_url": "https://github.com/graphql-hive/router.git",
"collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
"commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
"compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
"contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
"created_at": "2024-11-20T16:16:12Z",
"default_branch": "main",
"delete_branch_on_merge": true,
"deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
"description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
"events_url": "https://api.github.com/repos/graphql-hive/router/events",
"fork": false,
"forks": 16,
"forks_count": 16,
"forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
"full_name": "graphql-hive/router",
"git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
"git_url": "git://github.com/graphql-hive/router.git",
"has_discussions": false,
"has_downloads": false,
"has_issues": true,
"has_pages": false,
"has_projects": false,
"has_pull_requests": true,
"has_wiki": false,
"homepage": "https://the-guild.dev/graphql/hive/docs/router",
"hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
"html_url": "https://github.com/graphql-hive/router",
"id": 891604244,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
"issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
"keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
"labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"merge_commit_message": "PR_TITLE",
"merge_commit_title": "MERGE_MESSAGE",
"merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
"milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
"mirror_url": null,
"name": "router",
"node_id": "R_kgDONSTNFA",
"notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
"open_issues": 62,
"open_issues_count": 62,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
},
"private": false,
"pull_request_creation_policy": "all",
"pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
"pushed_at": "2026-07-21T18:20:18Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 10502,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 94,
"stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
"subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
"svn_url": "https://github.com/graphql-hive/router",
"tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
"teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
"topics": [
"apollo-federation",
"federation",
"federation-gateway",
"graphql",
"graphql-federation",
"router"
],
"trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
"updated_at": "2026-07-21T08:44:41Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 94,
"watchers_count": 94,
"web_commit_signoff_required": false
},
"sha": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
"user": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
}
},
"html_url": "https://github.com/graphql-hive/router/pull/1284",
"id": 4064208307,
"issue_url": "https://api.github.com/repos/graphql-hive/router/issues/1284",
"labels": [],
"locked": false,
"maintainer_can_modify": false,
"merge_commit_sha": "8670aa63c5afc721ef7464959d76115224dedb03",
"mergeable": null,
"mergeable_state": "unknown",
"merged": false,
"merged_at": null,
"merged_by": null,
"milestone": null,
"node_id": "PR_kwDONSTNFM7yPuWz",
"number": 1284,
"patch_url": "https://github.com/graphql-hive/router/pull/1284.patch",
"rebaseable": null,
"requested_reviewers": [
{
"avatar_url": "https://avatars.githubusercontent.com/u/8167190?v=4",
"events_url": "https://api.github.com/users/kamilkisiela/events{/privacy}",
"followers_url": "https://api.github.com/users/kamilkisiela/followers",
"following_url": "https://api.github.com/users/kamilkisiela/following{/other_user}",
"gists_url": "https://api.github.com/users/kamilkisiela/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/kamilkisiela",
"id": 8167190,
"login": "kamilkisiela",
"node_id": "MDQ6VXNlcjgxNjcxOTA=",
"organizations_url": "https://api.github.com/users/kamilkisiela/orgs",
"received_events_url": "https://api.github.com/users/kamilkisiela/received_events",
"repos_url": "https://api.github.com/users/kamilkisiela/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/kamilkisiela/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/kamilkisiela/subscriptions",
"type": "User",
"url": "https://api.github.com/users/kamilkisiela",
"user_view_type": "public"
}
],
"requested_teams": [],
"review_comment_url": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}",
"review_comments": 21,
"review_comments_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments",
"state": "open",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343",
"title": "feat(router): request selected supergraphs",
"updated_at": "2026-07-21T18:20:20Z",
"url": "https://api.github.com/repos/graphql-hive/router/pulls/1284",
"user": {
"avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
"events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
"followers_url": "https://api.github.com/users/enisdenjo/followers",
"following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
"gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/enisdenjo",
"id": 11807600,
"login": "enisdenjo",
"node_id": "MDQ6VXNlcjExODA3NjAw",
"organizations_url": "https://api.github.com/users/enisdenjo/orgs",
"received_events_url": "https://api.github.com/users/enisdenjo/received_events",
"repos_url": "https://api.github.com/users/enisdenjo/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
"type": "User",
"url": "https://api.github.com/users/enisdenjo",
"user_view_type": "public"
}
},
"repository": {
"allow_forking": true,
"archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
"blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
"clone_url": "https://github.com/graphql-hive/router.git",
"collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
"commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
"compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
"contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
"created_at": "2024-11-20T16:16:12Z",
"custom_properties": {
"vanta_production_branch_name": "main"
},
"default_branch": "main",
"deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
"description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
"events_url": "https://api.github.com/repos/graphql-hive/router/events",
"fork": false,
"forks": 16,
"forks_count": 16,
"forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
"full_name": "graphql-hive/router",
"git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
"git_url": "git://github.com/graphql-hive/router.git",
"has_discussions": false,
"has_downloads": false,
"has_issues": true,
"has_pages": false,
"has_projects": false,
"has_pull_requests": true,
"has_wiki": false,
"homepage": "https://the-guild.dev/graphql/hive/docs/router",
"hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
"html_url": "https://github.com/graphql-hive/router",
"id": 891604244,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
"issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
"keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
"labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
"milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
"mirror_url": null,
"name": "router",
"node_id": "R_kgDONSTNFA",
"notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
"open_issues": 62,
"open_issues_count": 62,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
},
"private": false,
"pull_request_creation_policy": "all",
"pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
"pushed_at": "2026-07-21T18:20:18Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 10502,
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 94,
"stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
"subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
"svn_url": "https://github.com/graphql-hive/router",
"tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
"teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
"topics": [
"apollo-federation",
"federation",
"federation-gateway",
"graphql",
"graphql-federation",
"router"
],
"trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
"updated_at": "2026-07-21T08:44:41Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"visibility": "public",
"watchers": 94,
"watchers_count": 94,
"web_commit_signoff_required": false
},
"sender": {
"avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
"events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
"followers_url": "https://api.github.com/users/enisdenjo/followers",
"following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
"gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/enisdenjo",
"id": 11807600,
"login": "enisdenjo",
"node_id": "MDQ6VXNlcjExODA3NjAw",
"organizations_url": "https://api.github.com/users/enisdenjo/orgs",
"received_events_url": "https://api.github.com/users/enisdenjo/received_events",
"repos_url": "https://api.github.com/users/enisdenjo/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
"type": "User",
"url": "https://api.github.com/users/enisdenjo",
"user_view_type": "public"
}
},
"github_job": "docker",
"github_ref": "refs/pull/1284/merge",
"github_ref_name": "1284/merge",
"github_ref_protected": "false",
"github_ref_type": "branch",
"github_repository": "graphql-hive/router",
"github_repository_id": "891604244",
"github_repository_owner": "graphql-hive",
"github_repository_owner_id": "182742256",
"github_run_attempt": "1",
"github_run_id": "29856788774",
"github_run_number": "4514",
"github_runner_arch": "X64",
"github_runner_environment": "github-hosted",
"github_runner_image_os": "ubuntu24",
"github_runner_image_version": "20260714.240.1",
"github_runner_name": "GitHub Actions 1000922431",
"github_runner_os": "Linux",
"github_runner_tracking_id": "github_73f16072-67a6-4c6d-91cb-88ad758944e9",
"github_server_url": "https://github.com",
"github_triggering_actor": "enisdenjo",
"github_workflow": "build-router",
"github_workflow_ref": "graphql-hive/router/.github/workflows/build-router.yaml@refs/pull/1284/merge",
"github_workflow_sha": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"platform": "linux/amd64"
}
}
},
"buildx.build.provenance/linux/arm64": {
"builder": {
"id": "https://github.com/graphql-hive/router/actions/runs/29856788774/attempts/1"
},
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/docker/dockerfile@1.22",
"digest": {
"sha256": "4a43a54dd1fedceb30ba47e76cfcf2b47304f4161c0caeac2db1c61804ea3c91"
}
},
{
"uri": "pkg:docker/gcr.io/distroless/cc-debian12@latest?platform=linux%2Farm64",
"digest": {
"sha256": "e8e7ee4b8b106d4c5fde9e422a321b2b8a2d5cca546c97adcce927f3e1d36e36"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "router.Dockerfile"
},
"parameters": {
"frontend": "gateway.v0",
"args": {
"cmdline": "docker/dockerfile:1.22",
"label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
"label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"label:org.opencontainers.image.licenses": "MIT",
"label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.title": "router",
"label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.vendor": "theguild",
"label:org.opencontainers.image.version": "pr-1284",
"source": "docker/dockerfile:1.22"
},
"locals": [
{
"name": "context"
},
{
"name": "dockerfile"
}
],
"root": {
"configSource": {
"path": "router.Dockerfile"
},
"request": {
"args": {
"label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
"label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"label:org.opencontainers.image.licenses": "MIT",
"label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.title": "router",
"label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
"label:org.opencontainers.image.vendor": "theguild",
"label:org.opencontainers.image.version": "pr-1284",
"vcs:localdir:context": ".",
"vcs:localdir:dockerfile": "docker",
"vcs:revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"vcs:source": "https://github.com/graphql-hive/router"
}
}
},
"compatibilityVersion": 30
},
"environment": {
"github_actor": "enisdenjo",
"github_actor_id": "11807600",
"github_event_name": "pull_request",
"github_event_payload": {
"action": "synchronize",
"after": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
"before": "d1d3845513e7b07401a41f74d256700c85f5988f",
"enterprise": {
"avatar_url": "https://avatars.githubusercontent.com/b/187753?v=4",
"created_at": "2024-07-02T08:52:28Z",
"description": "",
"html_url": "https://github.com/enterprises/the-guild",
"id": 187753,
"name": "The Guild",
"node_id": "E_kgDOAALdaQ",
"slug": "the-guild",
"updated_at": "2026-07-11T07:16:45Z",
"website_url": "https://the-guild.dev/"
},
"number": 1284,
"organization": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"description": "Schema registry, analytics and gateway for GraphQL federation and other GraphQL APIs.",
"events_url": "https://api.github.com/orgs/graphql-hive/events",
"hooks_url": "https://api.github.com/orgs/graphql-hive/hooks",
"id": 182742256,
"issues_url": "https://api.github.com/orgs/graphql-hive/issues",
"login": "graphql-hive",
"members_url": "https://api.github.com/orgs/graphql-hive/members{/member}",
"node_id": "O_kgDOCuRs8A",
"public_members_url": "https://api.github.com/orgs/graphql-hive/public_members{/member}",
"repos_url": "https://api.github.com/orgs/graphql-hive/repos",
"url": "https://api.github.com/orgs/graphql-hive"
},
"pull_request": {
"_links": {
"comments": {
"href": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments"
},
"commits": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits"
},
"html": {
"href": "https://github.com/graphql-hive/router/pull/1284"
},
"issue": {
"href": "https://api.github.com/repos/graphql-hive/router/issues/1284"
},
"review_comment": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}"
},
"review_comments": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments"
},
"self": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/1284"
},
"statuses": {
"href": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343"
}
},
"active_lock_reason": null,
"additions": 2435,
"assignee": null,
"assignees": [],
"author_association": "MEMBER",
"auto_merge": null,
"base": {
"label": "graphql-hive:main",
"ref": "main",
"repo": {
"allow_auto_merge": false,
"allow_forking": true,
"allow_merge_commit": false,
"allow_rebase_merge": false,
"allow_squash_merge": true,
"allow_update_branch": true,
"archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
"blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
"clone_url": "https://github.com/graphql-hive/router.git",
"collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
"commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
"compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
"contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
"created_at": "2024-11-20T16:16:12Z",
"default_branch": "main",
"delete_branch_on_merge": true,
"deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
"description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
"events_url": "https://api.github.com/repos/graphql-hive/router/events",
"fork": false,
"forks": 16,
"forks_count": 16,
"forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
"full_name": "graphql-hive/router",
"git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
"git_url": "git://github.com/graphql-hive/router.git",
"has_discussions": false,
"has_downloads": false,
"has_issues": true,
"has_pages": false,
"has_projects": false,
"has_pull_requests": true,
"has_wiki": false,
"homepage": "https://the-guild.dev/graphql/hive/docs/router",
"hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
"html_url": "https://github.com/graphql-hive/router",
"id": 891604244,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
"issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
"keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
"labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"merge_commit_message": "PR_TITLE",
"merge_commit_title": "MERGE_MESSAGE",
"merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
"milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
"mirror_url": null,
"name": "router",
"node_id": "R_kgDONSTNFA",
"notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
"open_issues": 62,
"open_issues_count": 62,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
},
"private": false,
"pull_request_creation_policy": "all",
"pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
"pushed_at": "2026-07-21T18:20:18Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 10502,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 94,
"stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
"subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
"svn_url": "https://github.com/graphql-hive/router",
"tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
"teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
"topics": [
"apollo-federation",
"federation",
"federation-gateway",
"graphql",
"graphql-federation",
"router"
],
"trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
"updated_at": "2026-07-21T08:44:41Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 94,
"watchers_count": 94,
"web_commit_signoff_required": false
},
"sha": "a822fe117fb035d300bed69f28b140bf57efb446",
"user": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
}
},
"body": "Supersedes and therefore closes graphql-hive/router#1269, closes graphql-hive/router#1137, addresses graphql-hive/router#524 through a plugin\n\nAllow a plugin to select a supergraph document during on_http_request and have it hold for each HTTP request or WebSocket upgrade. The selected supergraph is used consistently across validation, introspection, normalization, planning, authorization, demand control, execution, coprocessors, usage reporting, subscriptions, and request deduplication.\n\nThe implementation separates two kinds of state that were previously bundled together:\n\n* `Supergraph` (executor) owns schema-derived data that a plugin can safely construct and retain\n* `RouterSupergraphRuntime` (router) owns router-specific state derived from both that supergraph and the router configuration\n\nConfigured supergraphs still build their runtime eagerly before publication. Plugin-selected supergraphs build their runtime lazily on first use and reuse it through a bounded router-owned cache.\n\nAlso adding `supergraph.source: plugin`, where there is deliberately no configured fallback. A plugin must select a supergraph for each request that needs one.\n\n# Executor (public)\n\n## `Supergraph`\n\n`Supergraph` is the public owner handle. Plugins construct it with either:\n\n```rust\nSupergraph::from_sdl(...)\nSupergraph::from_document(...)\n```\n\nConstruction only accepts query-planner options. It does not accept router configuration, telemetry, callback subscription maps, HTTP clients, or other router-owned state.\n\nThe schema-derived data includes:\n\n* The parsed supergraph document\n* The public consumer schema and its SDL\n* The query planner and planner supergraph state\n* Schema metadata\n* A process-local cache identity\n\nPlugins retain an `Arc<Supergraph>` for as long as that variant should remain selectable. `OnHttpRequestHookPayload::set_supergraph` immediately takes a `SupergraphSnapshot` and stores that snapshot in request extensions. The router does not retain the plugin's `Arc<Supergraph>` owner.\n\nThis distinction is important. A request snapshot keeps the immutable schema data alive, but it does not keep the owner alive. The plugin remains the authority over whether new requests can select a variant!\n\n## Retirement\n\n`Supergraph` owns a `CancellationToken`. `SupergraphSnapshot` receives a clone of that token. When the last `Arc<Supergraph>` is dropped, `Supergraph::drop` cancels the token.\n\nThis gives retirement the required ownership semantics without callbacks from the executor crate into the router:\n\n1. The plugin removes or replaces an `Arc<Supergraph>` from its own state\n2. New requests can no longer obtain that owner\n3. Dropping the final owner publishes retirement\n4. Ordinary requests that already have a snapshot can finish\n5. Active subscriptions observe retirement and close with the schema-reload error\n6. The router's background cleanup task removes the retired runtime from its cache\n\nA snapshot or runtime never delays owner retirement. Conversely, retiring the owner does not invalidate immutable schema data already held by an ordinary in-flight request (they can finish).\n\n# Router (internal)\n\n## `RouterSupergraphRuntime`\n\n`RouterSupergraphRuntime` contains state that cannot be built by a plugin because it depends on router configuration or router-owned infrastructure:\n\n* The subgraph executor map\n* Operation-name forwarding configuration\n* Authorization metadata used by the router pipeline\n* Validation cache\n* Normalization cache\n* Query-plan cache\n* Demand-control runtime and formula cache\n\nRuntime construction receives a `SupergraphSnapshot`, the router configuration, telemetry, and the shared callback subscriptions map. It retains neither the `Supergraph` owner nor the snapshot. Requests and streams carry the snapshot separately.\n\nThis split keeps the public `Supergraph` portable and prevents plugins from having to construct fake `HiveRouterConfig`, `TelemetryContext`, callback maps, or router state.\n\n## Why caches live on the runtime\n\nMy original design shared schema-aware caches keyed by a supergraph cache ID. The final implementation instead places every schema-derived cache directly on its `RouterSupergraphRuntime`.\n\nThis is simpler and gives the same isolation guarantee structurally:\n\n* Two distinct `Supergraph` instances cannot share validation, normalization, plan, or demand-control formula entries because they have different runtimes\n* Cache keys dont need a supergraph ID because the cache itself belongs to exactly one runtime\n* Retiring or evicting a runtime naturally retires its cache entries with it\n* Configured schema rotation does not globally invalidate unrelated plugin caches\n* Old configured caches remain available only to requests or streams still holding the old runtime\n\nNote that the parse cache remains shared because parsing a GraphQL operation is schema-independent!\n\nThe process-local supergraph ID is still required for two purposes:\n\n* Looking up a plugin-selected runtime in the runtime cache\n* Partitioning in-flight request deduplication so identical operations against distinct supergraph instances never join the same execution\n\nIt's important to know that the ID is instance identity, not content identity! Two separately constructed supergraphs receive different IDs even if their SDL or consumer schemas are identical.\n\n# Configured and plugin-selected runtimes\n\n## Configured supergraph\n\n`SchemaState` has one atomic configured slot containing:\n\n* The configured `Arc<Supergraph>` owner\n* Its `SupergraphSnapshot`\n* Its eagerly built `Arc<RouterSupergraphRuntime>`\n\nThese values are published together with `ArcSwap`. A request can therefore never observe a schema from one generation and a runtime from another.\n\nThe configured runtime does not participate in the bounded plugin runtime cache. The current configured value is pinned by the configured slot and is never evicted. It follows the same ownership and retirement rules as a plugin variant, but its owner is retained by `SchemaState` instead of a plugin.\n\nConfigured loading and rotation follow this order:\n\n1. Load and parse the new supergraph SDL\n2. Run the existing supergraph load hooks\n3. Construct the schema-only `Supergraph`\n4. Run end hooks that may replace it\n5. Build `RouterSupergraphRuntime`\n6. Atomically publish owner, snapshot, and runtime\n7. Drop the previous configured owner after the swap\n\nIf schema or runtime construction fails, the current configured value remains untouched. A successful swap retires only the previous configured generation.\n\nSwapping no longer clears the complete shared schema caches or globally closes all subscriptions because dropping the old supergraph wil retire it and that will clean up its own cache and related subscriptions.\n\n## Plugin-selected supergraph\n\nPlugin runtimes are built lazily because the router cannot know which variants a plugin will select. `SchemaState` uses a `Mutex<VecDeque<...>>` as a strict FIFO runtime cache:\n\n* Maximum size is 10\n* The same supergraph ID reuses the same runtime\n* Cache hits do not change insertion order\n* Runtime construction is serialized under the mutex, so concurrent first requests build once\n* Failed construction is not inserted\n* Selecting a broken plugin variant fails closed and never falls back to the configured supergraph\n* Inserting an eleventh live variant evicts the oldest cached runtime\n\nFIFO eviction only removes the router's cached `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, publish retirement, or close subscriptions. If the plugin selects that still-live supergraph again, the router will rebuild its runtime.\n\n# Background runtime cleanup\n\nA router-managed `RuntimeCacheCleanupTask` receives registrations when plugin runtimes enter the cache. Each registration includes the supergraph ID and a clone of its retirement token. The task waits for retirement and removes the matching runtime entry.\n\nFIFO eviction sends a corresponding eviction message so the task can cancel and discard a waiter that no longer has a cached runtime to clean up. Registration is deduplicated by supergraph ID, including the case where a still-live supergraph is evicted and later inserted again.\n\nThis task has deliberately narrow authority:\n\n* Owner retirement can remove a runtime cache entry\n* FIFO eviction can remove a runtime cache entry\n* Runtime removal cannot retire a `Supergraph`\n* Runtime removal cannot stop an ordinary request or stream that already holds its own runtime `Arc`\n\nThe cache is still bounded if the cleanup task is unavailable. The task exists to release retired executors and schema caches earlier, not to provide the memory bound.\n\nConfigured runtimes do not need cleanup-task registration. Replacing the atomic configured slot immediately releases the slot's old runtime reference therefore dropping it. Requests and streams keep it alive only for as long as they still use it.\n\n## Eviction != retirement\n\nRuntime eviction and supergraph retirement are deliberately separate lifecycle events.\n\nThe plugin runtime cache stores only:\n\n```\nsupergraph ID -> Arc<RouterSupergraphRuntime>\n```\n\nIt does not store the owning `Arc<Supergraph>` or a complete `SelectedSupergraph`.\n\nEviction removes only the cache's `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, cancel the supergraph's retirement token, make the supergraph unselectable, or close its subscriptions.\n\nA running request or subscription holds its own SelectedSupergraph, which contains:\n\n* The `SupergraphSnapshot`\n* An `Arc<RouterSupergraphRuntime>`\n\nTherefore, evicting the cached runtime does not invalidate work already using it. The subscription's runtime remains alive through its own Arc, and the subscription continues normally.\n\nIf the plugin still owns the Supergraph and selects it again after its runtime was evicted, the router builds and caches a new runtime. Both runtimes are derived from the same immutable supergraph and router configuration, so this temporary overlap is safe.\n\nSubscriptions close with `SUBSCRIPTION_SCHEMA_RELOAD` **only** when the selected supergraph retires. Retirement occurs when the final owning Arc is dropped. `Supergraph::drop` cancels its retirement token, and subscription producers observing that token broadcast the reload error and stop. Existing subscriptions may continue using the previous runtime while new requests use the rebuilt runtime.\n\nThe background cleanup task may remove a cached runtime after its owner retires, but that removal is a consequence of retirement, not its cause. The retirement token closes subscriptions; cache cleanup only releases the router's cached runtime reference.\n\n# Request selection and consistency\n\n`SchemaState::select_supergraph` applies one selection rule:\n\n1. Reuse a `SelectedSupergraph` already pinned to the request (selecting multiple times throught)\n2. Otherwise prefer a plugin-provided `SupergraphSnapshot` from request extensions (plugin set)\n3. Otherwise use the configured owner, snapshot, and runtime (configuration set)\n4. If neither exists, return no selection\n5. If a plugin runtime cannot be built, return the runtime error without falling back\n * Deliberate decision, if a plugin provided a supergraph but cant be built - it would be a security issue to fall back to the configured one...\n\n`SelectedSupergraph` contains the exact snapshot and runtime pair. It is stored back into request extensions for both plugin and configured selections. This makes later users consume the same generation rather than reading the global configured slot again.\n\nThe selected pair is used by:\n\n* Validation and validation plugins\n* Progressive override state\n* GraphQL request and analysis coprocessors\n* Normalization\n* Variable coercion\n* Authorization\n* Query planning and query-plan plugins\n* Demand control\n* Introspection\n* Subgraph execution\n* Operation-name forwarding\n* Usage reporting\n* In-flight request deduplication\n* The GraphQL response coprocessor\n\nThe response coprocessor reads the pinned request selection. This matters during configured rotation because re-reading the current configured slot after execution could expose SDL from a newer generation than the one that produced the response.\n\n# Ordinary requests, deduplication, and subscriptions\n\n## Ordinary requests\n\nAn ordinary request keeps `SelectedSupergraph` alive through execution and response processing. If the owner retires during the request, the snapshot and runtime remain valid and the request completes normally.\n\nThis avoids aborting safe immutable work while still preventing future requests from selecting a removed owner.\n\n## In-flight request deduplication\n\nThe request fingerprint now includes the selected supergraph's instance ID instead of the consumer-schema checksum. Consumer schemas can be identical while routing supergraphs, subgraph endpoints, planner state, or ownership lifetimes differ.\n\nThis prevents requests for different supergraph instances from sharing an in-flight execution. Deduplicated subscription consumers still share one producer only when they selected the same supergraph instance and have the same remaining fingerprint inputs.\n\n## Subscription retirement\n\nThe subscription producer pump retains the complete `SelectedSupergraph`, not only its token. This keeps the selected snapshot and router runtime alive for the full stream lifetime, including after the HTTP handler has returned streaming headers.\n\nThe pump selects between the next upstream item and `SupergraphSnapshot::retired()`. Retirement broadcasts the existing error and stops the producer like before during schema reload.\n\nEach closes only subscriptions selected from the retired owner. The previous global `close_all_with_error` call on configured reload is removed.\n\n## WebSockets\n\nA valid WebSocket upgrade resolves and pins its `SelectedSupergraph` during the first message (not during the HTTP upgrade).\n\nAn upgrade without any selected or configured supergraph rejecting with HTTP 503 would be invisible to browsers because browsers dont provide insights to the WebSocket client about why the HTTP Upgrade failed.\n\nWe therefore instead accept the WebSocket connection and close it with a specific close code and message surfacing the error to the clients and giving insight into what happened.\n\nIf that supergraph later retires:\n\n* Existing subscription producers terminate through the retirement token\n* New operations on the connection are rejected as unavailable (explained above)\n* The connection cannot silently switch to a newly configured generation\n\nThis is the same lifetime rule as an ordinary request, extended to the WebSocket connection instead of one operation.\n\n# `supergraph.source: plugin`\n\nThe new configuration is:\n\n```yaml\nsupergraph:\n source: plugin\n```\n\nThis mode means there is no configured supergraph:\n\n* No file, Hive, or storage loader is created\n* No supergraph polling task is registered\n* The configured slot remains empty\n* Plugins, telemetry, caches, callback handling, and the HTTP server still initialize normally\n* A GraphQL request without a plugin-selected supergraph returns `NO_SUPERGRAPH_AVAILABLE` with HTTP 503\n* A WebSocket connection without a plugin-selected supergraph is closed with `No supergraph available yet`\n* A selected supergraph whose runtime cannot be built returns an internal runtime error\n\nExisting environment overrides for file and Hive sources continue to use the existing configuration override behavior. No plugin-source environment variable is added.\n\n# Health, readiness, and Prometheus\n\nHealth and readiness now pass through the plugin `on_http_request` chain and its `on_end` callbacks. This is required because readiness in plugin-only mode must allow the plugin to select a supergraph for that specific readiness request.\n\nProbe behavior is:\n\n* Health reports process liveness and remains 200 unless a plugin explicitly changes or ends the request\n* Readiness resolves the request's plugin-selected supergraph first and the configured fallback second\n* Readiness is 200 only when the selected supergraph is not retired and has a usable runtime\n* Missing selection or runtime construction failure produces 503\n* Coprocessors do not run for health or readiness\n* Prometheus keeps bypassing both plugins and coprocessors\n\nReadiness selection is request-local. A plugin selecting a supergraph for one readiness request does not publish it as a configured default.\n\n# Callback subscriptions\n\nAll configured and plugin-selected runtimes receive the same router-owned callback subscriptions map. The router also runs one heartbeat enforcer over that map.\n\nPlugins never construct or own this infrastructure. Sharing the map preserves callback routing and heartbeat enforcement regardless of which supergraph a request selected.\n\n# Hook and error ownership\n\nSupergraph load hooks continue to apply only to configured-source loading. They receive schema state without router-only executor state, and a plugin-selected `Supergraph` does not trigger configured reload hooks.\n\nError ownership follows the schema/runtime split:\n\n* SDL parsing and planner construction errors come from `Supergraph` construction\n* Subgraph executor and authorization runtime construction errors come from `RouterSupergraphRuntime` construction\n* Missing selection uses `PipelineError::NoSupergraphAvailable`\n* A plugin runtime build failure uses `PipelineError::RouterSupergraphRuntimeError` and is not cached\n\n`OnGraphQLValidationStartHookPayload::with_schema` is removed because it cannot provide pipeline-wide consistency. Plugins should migrate to `set_supergraph` in `on_http_request`.\n\n# Examples\n\n## `plugin_examples/replace_schema`\n\nThis example has a normal configured file supergraph. The plugin builds and retains one additional `Arc<Supergraph>` during initialization.\n\nRequests without the `x-schema-variant: basic` header use the configured default. Requests with that header select the plugin's stripped `basic` supergraph. The example demonstrates request-local override with a configured fallback, including validation and introspection behavior.\n\nIt also covers the fail-closed rule: selecting a variant whose router runtime cannot be constructed returns a runtime error instead of falling back to the configured schema.\n\n## `plugin_examples/feature_flags`\n\nThis example uses:\n\n```yaml\nsupergraph:\n source: plugin\n```\n\nThere is no configured default. The plugin owns the base document and a map of `Arc<Supergraph>` variants. It normalizes the feature-flag header, constructs each variant once, retains it in the map, and selects it with `set_supergraph`.\n\nEvery GraphQL request and readiness request must receive a plugin selection. If the plugin intentionally skips selection, the router returns `NO_SUPERGRAPH_AVAILABLE`. The example demonstrates full plugin ownership and switching among multiple schemas without any configured fallback.\n\nTogether the examples cover both supported models: override a configured default, or make the plugin the only supergraph source.\n\n# Intentional tradeoffs\n\n* Runtime construction is serialized on cache misses. Misses should be rare, and this guarantees one construction without another single-flight abstraction\n* The plugin runtime cache uses strict FIFO with a fixed capacity of 10. Hits do not refresh order, keeping behavior deterministic\n* Failed runtime construction is retried on a later request. Negative caching is deferred until repeated deterministic failures are shown to be a real problem\n* Retirement is based on owner lifetime, not SDL equality. Rebuilding identical SDL creates a new lifecycle and a new deduplication identity\n* Ordinary in-flight requests are allowed to finish after retirement. Only long-lived subscription work is actively terminated\n* The configured runtime is not placed in the plugin FIFO cache. Its atomic slot already provides the required permanent pin and rotation boundary\n\n# TODOs\n\n- [ ] docs\n- [ ] ~~rename ~~`~~SchemaState~~`~~ because its confusing, or merge it with ~~`~~RouterSharedState~~` for later times\n- [X] rename `new_supergraph_data` in `OnSupergraphLoadEndHookPayload` to just `new_supergraph` because it points to `Supergraph`. or should it point to `SupergraphData` still?\n- [X] (we shouldnt) should we expose the actual supergraph runtime error? I think it should be exposed unless masking is disabled (future)\n- [ ] ~~subgraph url overrides should have sort of a label that will allow users to override with dynamic supergraphs~~ think and decide about labeling in the future. for now, when the plugins are the only one that can dhynamically switch the supergraph - they have full control by managing their own map of the supergraphs, i.e. subgraph url overrides can happen directly in the plugin",
"changed_files": 43,
"closed_at": null,
"comments": 3,
"comments_url": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments",
"commits": 42,
"commits_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits",
"created_at": "2026-07-15T22:43:28Z",
"deletions": 623,
"diff_url": "https://github.com/graphql-hive/router/pull/1284.diff",
"draft": false,
"head": {
"label": "graphql-hive:super-replace-schemastate",
"ref": "super-replace-schemastate",
"repo": {
"allow_auto_merge": false,
"allow_forking": true,
"allow_merge_commit": false,
"allow_rebase_merge": false,
"allow_squash_merge": true,
"allow_update_branch": true,
"archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
"blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
"clone_url": "https://github.com/graphql-hive/router.git",
"collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
"commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
"compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
"contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
"created_at": "2024-11-20T16:16:12Z",
"default_branch": "main",
"delete_branch_on_merge": true,
"deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
"description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
"events_url": "https://api.github.com/repos/graphql-hive/router/events",
"fork": false,
"forks": 16,
"forks_count": 16,
"forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
"full_name": "graphql-hive/router",
"git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
"git_url": "git://github.com/graphql-hive/router.git",
"has_discussions": false,
"has_downloads": false,
"has_issues": true,
"has_pages": false,
"has_projects": false,
"has_pull_requests": true,
"has_wiki": false,
"homepage": "https://the-guild.dev/graphql/hive/docs/router",
"hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
"html_url": "https://github.com/graphql-hive/router",
"id": 891604244,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
"issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
"keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
"labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"merge_commit_message": "PR_TITLE",
"merge_commit_title": "MERGE_MESSAGE",
"merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
"milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
"mirror_url": null,
"name": "router",
"node_id": "R_kgDONSTNFA",
"notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
"open_issues": 62,
"open_issues_count": 62,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
},
"private": false,
"pull_request_creation_policy": "all",
"pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
"pushed_at": "2026-07-21T18:20:18Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 10502,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 94,
"stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
"subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
"svn_url": "https://github.com/graphql-hive/router",
"tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
"teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
"topics": [
"apollo-federation",
"federation",
"federation-gateway",
"graphql",
"graphql-federation",
"router"
],
"trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
"updated_at": "2026-07-21T08:44:41Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 94,
"watchers_count": 94,
"web_commit_signoff_required": false
},
"sha": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
"user": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
}
},
"html_url": "https://github.com/graphql-hive/router/pull/1284",
"id": 4064208307,
"issue_url": "https://api.github.com/repos/graphql-hive/router/issues/1284",
"labels": [],
"locked": false,
"maintainer_can_modify": false,
"merge_commit_sha": "8670aa63c5afc721ef7464959d76115224dedb03",
"mergeable": null,
"mergeable_state": "unknown",
"merged": false,
"merged_at": null,
"merged_by": null,
"milestone": null,
"node_id": "PR_kwDONSTNFM7yPuWz",
"number": 1284,
"patch_url": "https://github.com/graphql-hive/router/pull/1284.patch",
"rebaseable": null,
"requested_reviewers": [
{
"avatar_url": "https://avatars.githubusercontent.com/u/8167190?v=4",
"events_url": "https://api.github.com/users/kamilkisiela/events{/privacy}",
"followers_url": "https://api.github.com/users/kamilkisiela/followers",
"following_url": "https://api.github.com/users/kamilkisiela/following{/other_user}",
"gists_url": "https://api.github.com/users/kamilkisiela/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/kamilkisiela",
"id": 8167190,
"login": "kamilkisiela",
"node_id": "MDQ6VXNlcjgxNjcxOTA=",
"organizations_url": "https://api.github.com/users/kamilkisiela/orgs",
"received_events_url": "https://api.github.com/users/kamilkisiela/received_events",
"repos_url": "https://api.github.com/users/kamilkisiela/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/kamilkisiela/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/kamilkisiela/subscriptions",
"type": "User",
"url": "https://api.github.com/users/kamilkisiela",
"user_view_type": "public"
}
],
"requested_teams": [],
"review_comment_url": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}",
"review_comments": 21,
"review_comments_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments",
"state": "open",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343",
"title": "feat(router): request selected supergraphs",
"updated_at": "2026-07-21T18:20:20Z",
"url": "https://api.github.com/repos/graphql-hive/router/pulls/1284",
"user": {
"avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
"events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
"followers_url": "https://api.github.com/users/enisdenjo/followers",
"following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
"gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/enisdenjo",
"id": 11807600,
"login": "enisdenjo",
"node_id": "MDQ6VXNlcjExODA3NjAw",
"organizations_url": "https://api.github.com/users/enisdenjo/orgs",
"received_events_url": "https://api.github.com/users/enisdenjo/received_events",
"repos_url": "https://api.github.com/users/enisdenjo/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
"type": "User",
"url": "https://api.github.com/users/enisdenjo",
"user_view_type": "public"
}
},
"repository": {
"allow_forking": true,
"archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
"blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
"clone_url": "https://github.com/graphql-hive/router.git",
"collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
"commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
"compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
"contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
"created_at": "2024-11-20T16:16:12Z",
"custom_properties": {
"vanta_production_branch_name": "main"
},
"default_branch": "main",
"deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
"description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
"events_url": "https://api.github.com/repos/graphql-hive/router/events",
"fork": false,
"forks": 16,
"forks_count": 16,
"forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
"full_name": "graphql-hive/router",
"git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
"git_url": "git://github.com/graphql-hive/router.git",
"has_discussions": false,
"has_downloads": false,
"has_issues": true,
"has_pages": false,
"has_projects": false,
"has_pull_requests": true,
"has_wiki": false,
"homepage": "https://the-guild.dev/graphql/hive/docs/router",
"hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
"html_url": "https://github.com/graphql-hive/router",
"id": 891604244,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
"issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
"keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
"labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
"milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
"mirror_url": null,
"name": "router",
"node_id": "R_kgDONSTNFA",
"notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
"open_issues": 62,
"open_issues_count": 62,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
"events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
"followers_url": "https://api.github.com/users/graphql-hive/followers",
"following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
"gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/graphql-hive",
"id": 182742256,
"login": "graphql-hive",
"node_id": "O_kgDOCuRs8A",
"organizations_url": "https://api.github.com/users/graphql-hive/orgs",
"received_events_url": "https://api.github.com/users/graphql-hive/received_events",
"repos_url": "https://api.github.com/users/graphql-hive/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/graphql-hive",
"user_view_type": "public"
},
"private": false,
"pull_request_creation_policy": "all",
"pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
"pushed_at": "2026-07-21T18:20:18Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 10502,
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 94,
"stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
"subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
"svn_url": "https://github.com/graphql-hive/router",
"tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
"teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
"topics": [
"apollo-federation",
"federation",
"federation-gateway",
"graphql",
"graphql-federation",
"router"
],
"trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
"updated_at": "2026-07-21T08:44:41Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"visibility": "public",
"watchers": 94,
"watchers_count": 94,
"web_commit_signoff_required": false
},
"sender": {
"avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
"events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
"followers_url": "https://api.github.com/users/enisdenjo/followers",
"following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
"gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/enisdenjo",
"id": 11807600,
"login": "enisdenjo",
"node_id": "MDQ6VXNlcjExODA3NjAw",
"organizations_url": "https://api.github.com/users/enisdenjo/orgs",
"received_events_url": "https://api.github.com/users/enisdenjo/received_events",
"repos_url": "https://api.github.com/users/enisdenjo/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
"type": "User",
"url": "https://api.github.com/users/enisdenjo",
"user_view_type": "public"
}
},
"github_job": "docker",
"github_ref": "refs/pull/1284/merge",
"github_ref_name": "1284/merge",
"github_ref_protected": "false",
"github_ref_type": "branch",
"github_repository": "graphql-hive/router",
"github_repository_id": "891604244",
"github_repository_owner": "graphql-hive",
"github_repository_owner_id": "182742256",
"github_run_attempt": "1",
"github_run_id": "29856788774",
"github_run_number": "4514",
"github_runner_arch": "X64",
"github_runner_environment": "github-hosted",
"github_runner_image_os": "ubuntu24",
"github_runner_image_version": "20260714.240.1",
"github_runner_name": "GitHub Actions 1000922431",
"github_runner_os": "Linux",
"github_runner_tracking_id": "github_73f16072-67a6-4c6d-91cb-88ad758944e9",
"github_server_url": "https://github.com",
"github_triggering_actor": "enisdenjo",
"github_workflow": "build-router",
"github_workflow_ref": "graphql-hive/router/.github/workflows/build-router.yaml@refs/pull/1284/merge",
"github_workflow_sha": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-d3113114-df27-4710-98f0-e55b25592baa/builder-d3113114-df27-4710-98f0-e55b25592baa0/bo129p1the0tk4wmb8kx9r3ts",
"containerimage.descriptor": {
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:4c16a50efc81a3a1c1f0cb2436b9b1a43a4a60bf61777ef9b30f37965b4b4e53",
"size": 1609
},
"containerimage.digest": "sha256:4c16a50efc81a3a1c1f0cb2436b9b1a43a4a60bf61777ef9b30f37965b4b4e53",
"image.name": "ghcr.io/graphql-hive/router:pr-1284,ghcr.io/graphql-hive/router:sha-2a2f734"
} |
# Conflicts: # bin/router/src/pipeline/authorization/mod.rs # bin/router/src/pipeline/error.rs # bin/router/src/pipeline/execution.rs # bin/router/src/pipeline/mod.rs # bin/router/src/pipeline/normalize.rs # plugin_examples/Cargo.lock # plugin_examples/Cargo.toml
|
Would it be possible to make |
|
making the idea/recommended approach at the moment is to move I/O out of the request hook. load the possible supergraphs during mind you, the I/O use case is very much valid, especially since blocking would stall an ntex worker. we hope to address async support in a follow-up PR where we can make the necessary API and executor decisions deliberately and thoughtfully |
Supersedes and therefore closes #1269, closes #1137, addresses #524 through a plugin
Allow a plugin to select a supergraph document during on_http_request and have it hold for each HTTP request or WebSocket upgrade. The selected supergraph is used consistently across validation, introspection, normalization, planning, authorization, demand control, execution, coprocessors, usage reporting, subscriptions, and request deduplication.
The implementation separates two kinds of state that were previously bundled together:
Supergraph(executor) owns schema-derived data that a plugin can safely construct and retainRouterSupergraphRuntime(router) owns router-specific state derived from both that supergraph and the router configurationConfigured supergraphs still build their runtime eagerly before publication. Plugin-selected supergraphs build their runtime lazily on first use and reuse it through a bounded router-owned cache.
Also adding
supergraph.source: plugin, where there is deliberately no configured fallback. A plugin must select a supergraph for each request that needs one.Executor (public)
SupergraphSupergraphis the public owner handle. Plugins construct it with either:Construction only accepts query-planner options. It does not accept router configuration, telemetry, callback subscription maps, HTTP clients, or other router-owned state.
The schema-derived data includes:
Plugins retain an
Arc<Supergraph>for as long as that variant should remain selectable.OnHttpRequestHookPayload::set_supergraphimmediately takes aSupergraphSnapshotand stores that snapshot in request extensions. The router does not retain the plugin'sArc<Supergraph>owner.This distinction is important. A request snapshot keeps the immutable schema data alive, but it does not keep the owner alive. The plugin remains the authority over whether new requests can select a variant!
Retirement
Supergraphowns aCancellationToken.SupergraphSnapshotreceives a clone of that token. When the lastArc<Supergraph>is dropped,Supergraph::dropcancels the token.This gives retirement the required ownership semantics without callbacks from the executor crate into the router:
Arc<Supergraph>from its own stateA snapshot or runtime never delays owner retirement. Conversely, retiring the owner does not invalidate immutable schema data already held by an ordinary in-flight request (they can finish).
Router (internal)
RouterSupergraphRuntimeRouterSupergraphRuntimecontains state that cannot be built by a plugin because it depends on router configuration or router-owned infrastructure:Runtime construction receives a
SupergraphSnapshot, the router configuration, telemetry, and the shared callback subscriptions map. It retains neither theSupergraphowner nor the snapshot. Requests and streams carry the snapshot separately.This split keeps the public
Supergraphportable and prevents plugins from having to construct fakeHiveRouterConfig,TelemetryContext, callback maps, or router state.Why caches live on the runtime
My original design shared schema-aware caches keyed by a supergraph cache ID. The final implementation instead places every schema-derived cache directly on its
RouterSupergraphRuntime.This is simpler and gives the same isolation guarantee structurally:
Supergraphinstances cannot share validation, normalization, plan, or demand-control formula entries because they have different runtimesNote that the parse cache remains shared because parsing a GraphQL operation is schema-independent!
The process-local supergraph ID is still required for two purposes:
It's important to know that the ID is instance identity, not content identity! Two separately constructed supergraphs receive different IDs even if their SDL or consumer schemas are identical.
Configured and plugin-selected runtimes
Configured supergraph
SchemaStatehas one atomic configured slot containing:Arc<Supergraph>ownerSupergraphSnapshotArc<RouterSupergraphRuntime>These values are published together with
ArcSwap. A request can therefore never observe a schema from one generation and a runtime from another.The configured runtime does not participate in the bounded plugin runtime cache. The current configured value is pinned by the configured slot and is never evicted. It follows the same ownership and retirement rules as a plugin variant, but its owner is retained by
SchemaStateinstead of a plugin.Configured loading and rotation follow this order:
SupergraphRouterSupergraphRuntimeIf schema or runtime construction fails, the current configured value remains untouched. A successful swap retires only the previous configured generation.
Swapping no longer clears the complete shared schema caches or globally closes all subscriptions because dropping the old supergraph wil retire it and that will clean up its own cache and related subscriptions.
Plugin-selected supergraph
Plugin runtimes are built lazily because the router cannot know which variants a plugin will select.
SchemaStateuses aMutex<VecDeque<...>>as a strict FIFO runtime cache:FIFO eviction only removes the router's cached
Arc<RouterSupergraphRuntime>. It does not drop the plugin'sArc<Supergraph>, publish retirement, or close subscriptions. If the plugin selects that still-live supergraph again, the router will rebuild its runtime.Background runtime cleanup
A router-managed
RuntimeCacheCleanupTaskreceives registrations when plugin runtimes enter the cache. Each registration includes the supergraph ID and a clone of its retirement token. The task waits for retirement and removes the matching runtime entry.FIFO eviction sends a corresponding eviction message so the task can cancel and discard a waiter that no longer has a cached runtime to clean up. Registration is deduplicated by supergraph ID, including the case where a still-live supergraph is evicted and later inserted again.
This task has deliberately narrow authority:
SupergraphArcThe cache is still bounded if the cleanup task is unavailable. The task exists to release retired executors and schema caches earlier, not to provide the memory bound.
Configured runtimes do not need cleanup-task registration. Replacing the atomic configured slot immediately releases the slot's old runtime reference therefore dropping it. Requests and streams keep it alive only for as long as they still use it.
Eviction != retirement
Runtime eviction and supergraph retirement are deliberately separate lifecycle events.
The plugin runtime cache stores only:
It does not store the owning
Arc<Supergraph>or a completeSelectedSupergraph.Eviction removes only the cache's
Arc<RouterSupergraphRuntime>. It does not drop the plugin'sArc<Supergraph>, cancel the supergraph's retirement token, make the supergraph unselectable, or close its subscriptions.A running request or subscription holds its own SelectedSupergraph, which contains:
SupergraphSnapshotArc<RouterSupergraphRuntime>Therefore, evicting the cached runtime does not invalidate work already using it. The subscription's runtime remains alive through its own Arc, and the subscription continues normally.
If the plugin still owns the Supergraph and selects it again after its runtime was evicted, the router builds and caches a new runtime. Both runtimes are derived from the same immutable supergraph and router configuration, so this temporary overlap is safe.
Subscriptions close with
SUBSCRIPTION_SCHEMA_RELOADonly when the selected supergraph retires. Retirement occurs when the final owning Arc is dropped.Supergraph::dropcancels its retirement token, and subscription producers observing that token broadcast the reload error and stop. Existing subscriptions may continue using the previous runtime while new requests use the rebuilt runtime.The background cleanup task may remove a cached runtime after its owner retires, but that removal is a consequence of retirement, not its cause. The retirement token closes subscriptions; cache cleanup only releases the router's cached runtime reference.
Request selection and consistency
SchemaState::select_supergraphapplies one selection rule:SelectedSupergraphalready pinned to the request (selecting multiple times throught)SupergraphSnapshotfrom request extensions (plugin set)SelectedSupergraphcontains the exact snapshot and runtime pair. It is stored back into request extensions for both plugin and configured selections. This makes later users consume the same generation rather than reading the global configured slot again.The selected pair is used by:
The response coprocessor reads the pinned request selection. This matters during configured rotation because re-reading the current configured slot after execution could expose SDL from a newer generation than the one that produced the response.
Ordinary requests, deduplication, and subscriptions
Ordinary requests
An ordinary request keeps
SelectedSupergraphalive through execution and response processing. If the owner retires during the request, the snapshot and runtime remain valid and the request completes normally.This avoids aborting safe immutable work while still preventing future requests from selecting a removed owner.
In-flight request deduplication
The request fingerprint now includes the selected supergraph's instance ID instead of the consumer-schema checksum. Consumer schemas can be identical while routing supergraphs, subgraph endpoints, planner state, or ownership lifetimes differ.
This prevents requests for different supergraph instances from sharing an in-flight execution. Deduplicated subscription consumers still share one producer only when they selected the same supergraph instance and have the same remaining fingerprint inputs.
Subscription retirement
The subscription producer pump retains the complete
SelectedSupergraph, not only its token. This keeps the selected snapshot and router runtime alive for the full stream lifetime, including after the HTTP handler has returned streaming headers.The pump selects between the next upstream item and
SupergraphSnapshot::retired(). Retirement broadcasts the existing error and stops the producer like before during schema reload.Each closes only subscriptions selected from the retired owner. The previous global
close_all_with_errorcall on configured reload is removed.WebSockets
A valid WebSocket upgrade resolves and pins its
SelectedSupergraphduring the first message (not during the HTTP upgrade).An upgrade without any selected or configured supergraph rejecting with HTTP 503 would be invisible to browsers because browsers dont provide insights to the WebSocket client about why the HTTP Upgrade failed.
We therefore instead accept the WebSocket connection and close it with a specific close code and message surfacing the error to the clients and giving insight into what happened.
If that supergraph later retires:
This is the same lifetime rule as an ordinary request, extended to the WebSocket connection instead of one operation.
supergraph.source: pluginThe new configuration is:
This mode means there is no configured supergraph:
NO_SUPERGRAPH_AVAILABLEwith HTTP 503No supergraph available yetExisting environment overrides for file and Hive sources continue to use the existing configuration override behavior. No plugin-source environment variable is added.
Health, readiness, and Prometheus
Health and readiness now pass through the plugin
on_http_requestchain and itson_endcallbacks. This is required because readiness in plugin-only mode must allow the plugin to select a supergraph for that specific readiness request.Probe behavior is:
Readiness selection is request-local. A plugin selecting a supergraph for one readiness request does not publish it as a configured default.
Callback subscriptions
All configured and plugin-selected runtimes receive the same router-owned callback subscriptions map. The router also runs one heartbeat enforcer over that map.
Plugins never construct or own this infrastructure. Sharing the map preserves callback routing and heartbeat enforcement regardless of which supergraph a request selected.
Hook and error ownership
Supergraph load hooks continue to apply only to configured-source loading. They receive schema state without router-only executor state, and a plugin-selected
Supergraphdoes not trigger configured reload hooks.Error ownership follows the schema/runtime split:
SupergraphconstructionRouterSupergraphRuntimeconstructionPipelineError::NoSupergraphAvailablePipelineError::RouterSupergraphRuntimeErrorand is not cachedOnGraphQLValidationStartHookPayload::with_schemais removed because it cannot provide pipeline-wide consistency. Plugins should migrate toset_supergraphinon_http_request.Examples
plugin_examples/replace_schemaThis example has a normal configured file supergraph. The plugin builds and retains one additional
Arc<Supergraph>during initialization.Requests without the
x-schema-variant: basicheader use the configured default. Requests with that header select the plugin's strippedbasicsupergraph. The example demonstrates request-local override with a configured fallback, including validation and introspection behavior.It also covers the fail-closed rule: selecting a variant whose router runtime cannot be constructed returns a runtime error instead of falling back to the configured schema.
plugin_examples/feature_flagsThis example uses:
There is no configured default. The plugin owns the base document and a map of
Arc<Supergraph>variants. It normalizes the feature-flag header, constructs each variant once, retains it in the map, and selects it withset_supergraph.Every GraphQL request and readiness request must receive a plugin selection. If the plugin intentionally skips selection, the router returns
NO_SUPERGRAPH_AVAILABLE. The example demonstrates full plugin ownership and switching among multiple schemas without any configured fallback.Together the examples cover both supported models: override a configured default, or make the plugin the only supergraph source.
Intentional tradeoffs
TODOs
renameSchemaStatebecause its confusing, or merge it withRouterSharedStatefor later timesnew_supergraph_datainOnSupergraphLoadEndHookPayloadto justnew_supergraphbecause it points toSupergraph. or should it point toSupergraphDatastill?subgraph url overrides should have sort of a label that will allow users to override with dynamic supergraphsthink and decide about labeling in the future. for now, when the plugins are the only one that can dhynamically switch the supergraph - they have full control by managing their own map of the supergraphs, i.e. subgraph url overrides can happen directly in the plugin