diff --git a/bun.lock b/bun.lock index 5beb6c1e..58942b6c 100644 --- a/bun.lock +++ b/bun.lock @@ -97,6 +97,7 @@ "tailwind-merge": "3.4.0", "twoslash": "0.3.6", "vite": "7.3.1", + "zod": "4.3.6", }, "devDependencies": { "@playwright/test": "1.57.0", diff --git a/packages/documentation/content/docs/router/configuration/coprocessor.mdx b/packages/documentation/content/docs/router/configuration/coprocessor.mdx new file mode 100644 index 00000000..5609eb4d --- /dev/null +++ b/packages/documentation/content/docs/router/configuration/coprocessor.mdx @@ -0,0 +1,254 @@ +--- +title: "coprocessor" +--- + +The `coprocessor` configuration enables external request hooks in Hive Router. + +For docs and usage guidance, see +**[Coprocessors](/docs/router/customizations/coprocessors)**. + +## Top-level options + +### `url` + +- **Type:** `string` +- **Required:** Yes + +Endpoint of the external coprocessor service. + +Supported formats: + +- `http://host[:port][/path]` +- `unix:///absolute/path/to/socket.sock` +- `unix:///absolute/path/to/socket.sock?path=/request/path` + +### `protocol` + +- **Type:** `string` +- **Required:** Yes +- **Allowed values:** `http1`, `h2c`, `http2` + +Transport protocol used to call the coprocessor endpoint. + +- `http1` = HTTP/1.1 over TCP +- `h2c` = HTTP/2 cleartext over TCP +- `http2` is currently unsupported at runtime and rejected + +### `timeout` + +- **Type:** `string` +- **Default:** `1s` + +Timeout for a single coprocessor call. + +Examples: `100ms`, `1s`, `2s`. + +### `stages` + +- **Type:** `object` +- **Default:** `{}` + +Stage-specific configuration. + +Top-level keys: + +- `router` +- `graphql` + +## Stage structure + +```yaml title="router.config.yaml" +coprocessor: + url: http://127.0.0.1:8081/coprocessor + protocol: http1 + timeout: 1s + stages: + router: + request: {} + response: {} + graphql: + request: {} + analysis: {} + response: {} +``` + +All stages are optional. A stage runs only if its object is configured. + +--- + +## Stage options + +Each stage supports two keys: + +- `condition` +- `include` + +### `condition` + +- **Type:** `boolean | expression` +- **Default:** `true` + +If set, the stage runs only when the condition evaluates to `true`. + +Example: + +```yaml +stages: + router: + request: + condition: + expression: .request.method == "POST" +``` + +### `include` + +- **Type:** stage-specific object +- **Default:** all include fields disabled + +Selects which fields Hive Router sends to your coprocessor for that stage. + +Important: `include` controls what is sent to the coprocessor. It does not define all mutation +permissions on returned data. + +## `router` stage options + +### `router.request` + +```yaml +include: + body: true + headers: true + method: true + path: true + context: true # false or a list of keys +``` + +### `response.include` + +```yaml +include: + body: true + headers: true + status_code: true + context: true # false or a list of keys +``` + +## `graphql` stage options + +### `graphql.request` + +```yaml +include: + body: true # false or a list ["query", "operationName", "variables", "extensions"] + headers: true + method: true + path: true + sdl: true + context: true # false or a list of keys +``` + +### `graphql.analysis` + +```yaml +include: + body: true # false or a list ["query", "operationName", "variables", "extensions"] + headers: true + method: true + path: true + sdl: true + context: true # false or a list of keys +``` + +### `graphql.response` + +```yaml +include: + body: true # false + headers: true + status_code: true + sdl: true + context: true # false or a list ["key1", "key2"] +``` + +## Selection formats + +### `context` selection + +`context` supports three forms: + +- `false` - include no context +- `true` - include full context +- list of keys - include selected keys only + +```yaml +context: false +# or +context: true +# or +context: + - hive::progressive_override::labels_to_override + - hive::operation::name +``` + +### GraphQL `body` selection + +For `graphql.request.include.body` and `graphql.analysis.include.body`: + +- `false` - include no body fields +- `true` - include all body fields +- list - include selected body fields + +Allowed list values: + +- `query` +- `operationName` +- `variables` +- `extensions` + +```yaml +body: false +# or +body: true +# or +body: [query, variables, operationName] +``` + +## Full example + +```yaml title="router.config.yaml" +coprocessor: + url: unix:///tmp/hive-coprocessor.sock?path=/coprocessor + protocol: h2c + timeout: 1s + stages: + router: + request: + condition: + expression: .request.method == "POST" + include: + headers: true + method: true + path: true + context: + - hive::operation::name + response: + include: + headers: true + status_code: true + graphql: + request: + include: + body: [query, operationName, variables, extensions] + headers: true + context: true + analysis: + include: + body: [query] + context: + - your.custom_key + response: + include: + body: true + headers: true + status_code: true +``` diff --git a/packages/documentation/content/docs/router/customizations/coprocessors/index.mdx b/packages/documentation/content/docs/router/customizations/coprocessors/index.mdx new file mode 100644 index 00000000..66edead3 --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/coprocessors/index.mdx @@ -0,0 +1,187 @@ +--- +title: "Coprocessors" +sidebarTitle: "Introduction" +--- + +import { Callout } from "@hive/design-system/hive-components/callout"; + +Coprocessors are an external way to customize Hive Router. + +With coprocessors, Hive Router calls your HTTP service during request processing. This lets you add +custom logic without building a custom router binary. + +You can use this for policy checks, request changes, response redaction, and tenant-specific rules. +Your team can write this logic in any language. + +## When to use coprocessors + +Use coprocessors when you want custom behavior that is easy to deploy, update, and own outside the +router process. + +They are not the best choice for logic that must be very low latency. Every enabled stage makes a +network call. Keep payloads small, keep logic simple, and keep response time low. + +If you need maximum in-process performance and better APIs, use [Plugin System](/docs/router/customizations/plugin-system) instead. + +## The Concept of Coprocessors + +At a high level, a coprocessor is an external HTTP service that Hive Router calls at selected +points in the request lifecycle. + +This lets you keep custom logic outside the router process. Hive Router sends stage-specific JSON +payloads to your service, your service returns a JSON decision, and the router applies that decision +before continuing. + +The diagram below shows one concrete example using `graphql.analysis`. + +```mermaid +flowchart TB + A[Client Request] --> B[GraphQL parsing and validation] + B --> C[Stage: `graphql.analysis`] + C --> F[Inject active labels for Progressive Override] + F --> G[GraphQL execution] + G --> H[Client Response] + + CP[Coprocessor Service] + + C -- HTTP request --> CP + CP -- HTTP response --> F +``` + +For each enabled stage, Hive Router sends a JSON payload and waits for a response. + +The example above focuses on `graphql.analysis`. After parsing and validation, Hive Router sends an +HTTP call to your coprocessor service. The coprocessor can return [context](/docs/router/customizations/coprocessors/stage-and-protocol#request-context) updates used to inject +active labels for [Progressive Override](/docs/router/configuration/override_labels) before GraphQL execution starts. + +Your response decides if the router should continue and plan the query, or stop and return early. + +### Network + +Coprocessor calls run in the critical request path, so the location of the coprocessor service directly affects request latency. Run the coprocessor as close to Hive Router instance as possible. A shorter path means lower request latency. + +Hive Router supports two transport options: + +- `http://` for normal TCP networking +- `unix://` for Unix Domain Sockets (UDS) + +Use `http://` when the coprocessor is a shared or remote service. This is usually easier to operate, +but every stage call pays network cost (additional hop latency and network failure risk). + +Use `unix://` when the router and coprocessor run on the same node or pod. UDS avoids external +network hops and usually gives lower, more stable latency. + +It also supports different HTTP protocol modes: + +- `http1` for HTTP/1.1 +- `http2` for HTTP/2 over TCP (with TLS) +- `h2c` for HTTP/2 without TLS + + + Prefer Unix Domain Sockets (`unix://`) with `h2c` and run the coprocessor as a + sidecar for lowest latency. + + +### Continue or Break Processing + +Hive Router always expects a response from the coprocessor service, so when you simply want to continue, +without applying any changes to the state, send only `version` and `control` back to the router. + +```json title="Continue with no modifications" +{ + "version": 1, + "control": "continue" +} +``` + +It's possible to short-circut the request's lifecycle, with an early HTTP response. You simply pass an object with `"break"` property with a status code as its value. + + + It's highly recommended to include `body` and `headers` in the response. + + +```json title="Early 401 response" +{ + "version": 1, + "control": { "break": 401 }, + "headers": { + "content-type": "application/json" + }, + "body": { + "errors": [{ "message": "Unauthorized" }] + } +} +``` + +### Apply Changes + +To mute allowed fields like `headers` and `context`, include them in the response. + + + The `headers` object will override the headers of a request or response + (depending on the stage), so we recommend to include the original headers as + well. + + +```json title="Ovewrite headers and add key-value to context" +{ + "version": 1, + "control": "continue", + "headers": { + "x-custom-header": "your-value" + }, + "context": { + "hive::progressive_override::labels_to_override": ["feature-a"] + } +} +``` + +## Stages + +Each stage runs at a different point in the pipeline, +so each stage is best for a different kind of logic. + +### `router.request` + +Runs when the HTTP request enters Hive Router. +Use this stage for early traffic checks such as auth header checks and fast request rejection. + +### `graphql.request` + +Runs when GraphQL request payload is available and we know it's a GraphQL request. Use this stage for request shaping, variable +normalization, and request-level guardrails. + +### `graphql.analysis` + +Runs after GraphQL parsing and validation, but before query planning and execution. +This is the stage is used in the progressive override label-injection example above. + +### `graphql.response` + +Runs after GraphQL execution returns a GraphQL response. Use this stage for response normalization, +error shaping, and redaction before final HTTP response handling. + +### `router.response` + +Runs at the very end, right before the response is sent to the client. + +## Observability + +Each stage call produces telemetry: + +- Metrics expose per-stage throughput, latency, and failures. +- Traces include a `coprocessor` span that wraps each stage call (with `coprocessor.stage` and `coprocessor.id` attributes). +- Nested `http.client` spans show outbound calls to the coprocessor service (status code, latency, failures). +- Logs record lifecycle events with correlation fields (`coprocessor.stage`, `coprocessor.id`). + +This is important for production. Coprocessors are in the critical path. Monitor them like your +subgraphs. + +For telemetry setup, see [OpenTelemetry Metrics](/docs/router/observability/metrics#coprocessor) and +[OpenTelemetry Tracing](/docs/router/observability/tracing). + +## Learn more + +- [Get Started with Coprocessors](/docs/router/customizations/coprocessors/usage) +- [Stages and Protocol](/docs/router/customizations/coprocessors/stages-and-protocol) +- [Configuration Reference](/docs/router/configuration/coprocessor) diff --git a/packages/documentation/content/docs/router/customizations/coprocessors/meta.json b/packages/documentation/content/docs/router/customizations/coprocessors/meta.json new file mode 100644 index 00000000..560ed9f9 --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/coprocessors/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Coprocessors", + "pages": ["index", "usage", "stages-and-protocol"] +} diff --git a/packages/documentation/content/docs/router/customizations/coprocessors/stages-and-protocol.mdx b/packages/documentation/content/docs/router/customizations/coprocessors/stages-and-protocol.mdx new file mode 100644 index 00000000..5b97c6d8 --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/coprocessors/stages-and-protocol.mdx @@ -0,0 +1,416 @@ +--- +title: "Stages and Protocol" +--- + +import { Callout } from "@hive/design-system/hive-components/callout"; + +This page describes how Hive Router and a coprocessor service communicate. + +Each configured stage is translates to a request-response exchange. Hive Router sends a JSON payload, +your service returns a JSON decision, and the router applies that decision before continuing. + +Understanding this contract is key to building reliable and predictable coprocessors. + +If you need the YAML option reference, see +[**coprocessor configuration reference**](/docs/router/configuration/coprocessor). + +## Version + +Every coprocessor request and response must include a `version` field. The version identifies the protocol contract used by Hive Router. + +```json +{ + "version": 1, + "control": "continue" + // ... +} +``` + +Including an explicit version allows the protocol to evolve over time without breaking existing +integrations. Future versions may introduce new fields, stricter validation, or different mutation +rules. + + + If the version is missing or unsupported, Hive Router treats the response as a + coprocessor failure. + + +Hive Router always sends the current version in the request. Your coprocessor should echo the same +version in its response. + +## Control flow + +Each enabled stage sends a JSON payload to your coprocessor and waits for a JSON response. + +The response must include a `control` field that determines the next step: + +- `"continue"` - proceed to the next step in the pipeline +- `{ "break": }` - stop processing and return an immediate HTTP response + +### Continue + +In case you do not want to modify anything and just let the router proceed to the next step, send back only `version` and `control`. + +```json title="Simple continue decision" +{ + "version": 1, + "control": "continue" +} +``` + +### Continue with mutations + +However, if you want to modify the state to pass data to the next stages, adjust pipeline's behaviour or alter the request's body, you're able to send back more than just `version` and `control`. + +```json title="Response for 'graphql.request' stage event" +{ + "version": 1, + "control": "continue", + "headers": { + "x-custom-header": "value" + }, + "context": { + "custom.key": "value" + } +} +``` + +In the example above, the `"custom.key": "value"` will be inserted to the context and available for the next stages and plugin hooks. The headers will be dropped and replaced with only `"x-custom-header": "value` going forward. Every next step or a plugin hook, will receive new headers, that applies to header propagation feature as well. + +### Break (short-circuit) + +At every stage you're able to short-circuit the pipeline and return an early response. Be aware that sending back no `body` will result in an empty response for the client's request. + +```json title="Early response with 401 status code, body and headers" +{ + "version": 1, + "control": { "break": 401 }, + "headers": { + "content-type": "application/json" + }, + "body": { + "errors": [{ "message": "Unauthorized" }] + } +} +``` + +## Protocol envelope + +Stage payloads sent by Hive Router to Coprocessors always include these fields: + +- `version` (currently `1`) +- `stage` (current stage name) +- `control` (always starts as `"continue"`) +- `id` (request identifier) + +Depending on `include` settings, payloads may also contain fields like `headers`, `body`, `context`, `method`, `path`, `status_code`, and `sdl`. + + + Your coprocessor must return `OK 200` with valid JSON. Invalid JSON, invalid + `control` values, unsupported `version`, or forbidden stage mutations are + treated as coprocessor failures and **fail the entire client request**. + + +## Stages + +The following diagram describes the execution order and available stages. + +```mermaid +flowchart TB + A[Client Request] + SRReq[Stage: router.request] + B[Read GraphQL request] + C[Parse and validate GraphQL] + SGReq[Stage: graphql.request] + D[auth, JWT, policy, progressive labels] + SGA[Stage: graphql.analysis] + E[Plan GraphQL operation] + F[Execute query plan] + SGRes[Stage: graphql.response] + G[Build HTTP response] + SRRes[Stage: router.response] + M[Client Response] + + CP[Coprocessor Service] + + A --> SRReq + SRReq --> B + B --> C + C --> SGReq + SGReq --> D + D --> SGA + SGA --> E + E --> F + F --> SGRes + SGRes --> G + G --> SRRes + SRRes --> M + + SRReq -- HTTP request --> CP + CP -- HTTP response --> SRReq + + SGReq -- HTTP request --> CP + CP -- HTTP response --> SGReq + + SGA -- HTTP request --> CP + CP -- HTTP response --> SGA + + SGRes -- HTTP request --> CP + CP -- HTTP response --> SGRes + + SRRes -- HTTP request --> CP + CP -- HTTP response --> SRRes +``` + +### `router.request` + +Runs when the inbound HTTP request enters Hive Router. +Use this stage for early traffic checks such as auth header checks and fast request rejection. + +Available properties in `include`: + +| Property | Mutable | Description | +| --------- | :-----: | --------------------------------------------- | +| `headers` | ✅ | Headers of the client request | +| `method` | ❌ | HTTP method of the client request | +| `path` | ❌ | HTTP request's path of the client request | +| `body` | ✅ | I think you know already, HTTP request's body | +| `context` | ✅ | Serialized request context | + +### `graphql.request` + +Runs after the router recognizes GraphQL request data and extracts the GraphQL payload from either body or query params. +Use this stage for request shaping, variable normalization, and request-level guardrails. + +| Property | Mutable | Description | +| --------- | :-----: | ----------------------------------------------------------------------------------------- | +| `headers` | ✅ | Headers of the client request | +| `method` | ❌ | HTTP method of the client request | +| `path` | ❌ | HTTP request's path of the client request | +| `body` | ✅ | GraphQL request payload (contains `query`, `operationName`, `variables` and `extensions`) | +| `sdl` | ❌ | Text representation (SDL) of the public schema | +| `context` | ✅ | Serialized request context | + +### `graphql.analysis` + +Runs after GraphQL parsing and validation, but right before planning and execution. +This is the stage is used in the progressive override label-injection example above. + +| Property | Mutable | Description | +| --------- | :-----: | ----------------------------------------------------------------------------------------- | +| `headers` | ✅ | Headers of the client request | +| `method` | ❌ | HTTP method of the client request | +| `path` | ❌ | HTTP request's path of the client request | +| `body` | ❌ | GraphQL request payload (contains `query`, `operationName`, `variables` and `extensions`) | +| `sdl` | ❌ | Text representation (SDL) of the public schema | +| `context` | ✅ | Serialized request context | + +### `graphql.response` + +Runs after GraphQL execution produces a GraphQL response. +Use this stage for response normalization, error shaping, and redaction before final HTTP response handling. + +| Property | Mutable | Description | +| ------------- | :-----: | ---------------------------------------------- | +| `headers` | ✅ | Headers of the client response | +| `status_code` | ❌ | Status code of the client response | +| `body` | ✅ | GraphQL response | +| `sdl` | ❌ | Text representation (SDL) of the public schema | +| `context` | ✅ | Serialized request context | + +### `router.response` + +Runs right before the final HTTP response is sent to the client. + +| Property | Mutable | Description | +| ------------- | :-----: | ---------------------------------------------- | +| `headers` | ✅ | Headers of the client response | +| `status_code` | ❌ | Status code of the client response | +| `body` | ✅ | Body of the response | +| `sdl` | ❌ | Text representation (SDL) of the public schema | +| `context` | ✅ | Serialized request context | + +## Request context + +Request context is key-value state attached to a single request lifecycle and shared with built-in features, custom plugins and coprocessors. + +Use it to pass decisions between stages, for example: + +- extract `tenant.id` from headers in `router.request` +- read `tenant.id` in `graphql.analysis` to select feature flags or routing behavior + +Treat context keys as shared API contracts: + +- use clear namespacing (`hive::` is reserved) +- keep values small and serializable + +Context updates are propagated to later stages. + +The `include.context` property supports three forms: + +| Value | Action | +| ---------- | -------------------------- | +| `false` | Include no context | +| `true` | Include full context | +| `[string]` | Include only selected keys | + +### Operation + +| Property | Description | +| ----------------------- | ------------------------------------------------------------------------------ | +| `hive::operation::name` | The name of the GraphQL operation. | +| `hive::operation::kind` | The kind of the GraphQL operation (`"query"`, `"mutation"`, `"subscription"`). | + +### Authentication + +| Property | Description | +| ---------------------------------- | -------------------------------------------------------------------------------- | +| `hive::authentication::jwt_scopes` | Scopes extracted from the current authenticated user's JWT. | +| `hive::authentication::jwt_status` | Authentication status. If `true`, the request has been verified asauthenticated. | + +### Progressive Override + +| Property | Description | +| ------------------------------------------------ | ------------------------------------------------------------ | +| `hive::progressive_override::unresolved_labels` | The set of labels that require an external decision | +| `hive::progressive_override::labels_to_override` | The set of labels that should be overridden for this request | + +### Telemetry + +| Property | Mutable | Description | +| --------------------------------- | :-----: | ------------------------------------- | +| `hive::telemetry::client_name` | ✅ | The name of the client application | +| `hive::telemetry::client_version` | ✅ | The version of the client application | + +## Body + +- Some stages allow body mutation, but `graphql.analysis` does not. +- Body patches must stay valid for the stage contract. +- Invalid body patches are rejected. + +For `graphql.request` and `graphql.analysis`, `include.body` supports selective field inclusion: + +| Value | Action | +| ---------- | ---------------------------------------------------------------------------------------------------------------- | +| `false` | Include no body | +| `true` | Include full body | +| `[string]` | Include only selected properties
Available properties: `query`, `operationName`, `variables`, `extensions` | + +```yaml title="Selective body include" +stages: + graphql: + request: + include: + body: [query, variables] +``` + +You can also patch body fields selectively in your coprocessor response. +For example, in `graphql.request`, this response updates only variables: + +```json title="Updates variables only" +{ + "version": 1, + "control": "continue", + "body": { + "variables": { + "first": 20 + } + } +} +``` + +```json title="Overwrites 'extensions' that were never selected" +{ + "version": 1, + "control": "continue", + "body": { + "extensions": { + "code": "value" + } + } +} +``` + +## Headers + +The `headers` property represents either request or response headers, depending on the stage. Headers can be returned and mutated even when not included in inbound stage payload. + +```json title="Sets new request headers for in 'router.request' stage" +{ + "version": 1, + "control": "continue", + "headers": { + "content-type": "application/json", + "x-something": "true" + } +} +``` + +To mutate headers without overwriting them you need to set `include.headers: true` and return them back, modified: + +```js title="Pseudo code for mutating headers" +let headers = stage_payload.headers; +Response.json({ + version: 1, + control: "continue", + headers: { + ...headers, + "x-something": "true", + }, +}); +``` + +## Include vs mutation semantics + +The `include` property controls what Hive Router sends to your coprocessor in the request payload. It does not +strictly limit what fields your coprocessor can return for mutation. + +Examples: + +- `include.headers: false` can still return `headers` in response and overwrite headers. +- `include.context` may omit a key, but response can still patch that context key . + +```yaml title="Example: only operation's name is sent to coprocessor" +stages: + graphql: + request: + include: + context: ["hive::operation::name"] +``` + +Even with selective inclusion, your response may still patch additional non-included keys: + +```json +{ + "version": 1, + "control": "continue", + "context": { + "custom::b": "patched-value" + } +} +``` + +This is why `include` should be treated as payload minimization, not as mutation permission. + +## Failure behavior + +When a request from Hive Router to Coprocessor fails for some reason, the request lifecycle is stopped, the current progress is dropped and an HTTP response is returned to the client. + +Common coprocessor failure causes: + +- non-200 response from the coprocessor +- malformed JSON +- unsupported protocol `version` +- invalid `control` value +- forbidden stage mutation + +How client response looks like: + +- request fails (often HTTP `500`) +- GraphQL error message is masked as `Internal server error` +- codes remain in `extensions.code` + +--- + +- [Get started with Coprocessors](/docs/router/customizations/coprocessors/usage) +- [Coprocessor configuration reference](/docs/router/configuration/coprocessor) diff --git a/packages/documentation/content/docs/router/customizations/coprocessors/usage.mdx b/packages/documentation/content/docs/router/customizations/coprocessors/usage.mdx new file mode 100644 index 00000000..b96db3fc --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/coprocessors/usage.mdx @@ -0,0 +1,252 @@ +--- +title: "Writing a Coprocessor" +sidebarTitle: "Usage" +--- + +import { Callout } from "@hive/design-system/hive-components/callout"; +import { Step, Steps } from "fumadocs-ui/components/steps"; + +This guide walks you through creating your first coprocessor service and connecting it to Hive +Router. + +At the end, you will have a working setup where Hive Router calls your external service during +request processing, and your service can either continue the request or stop it early with a custom +response. + +## Before you start + +You need three things: + +- a running Hive Router +- a valid `supergraph.graphql` +- a small HTTP service that accepts and returns JSON + +If you do not have a supergraph yet, you can use a test supergraph: + +```bash +curl -sSL https://federation-demo.theguild.workers.dev/supergraph.graphql > supergraph.graphql +``` + +## Build your first coprocessor + + + + +### Create a local service + +Hive Router sends a JSON payload with fields like `version`, `stage`, `control`, and optional +request data. Your service should return JSON with at least: + +```json title="Minimal required response" +{ + "version": 1, + "control": "continue" +} +``` + +This tells the router to continue processing. + + + Return `OK 200` with valid JSON. Invalid responses are treated as coprocessor + failures. + + +Let's start by creating a small service in any language. It must expose one HTTP endpoint, for example +`POST /coprocessor`. + +For local testing, run it on `http://127.0.0.1:8081/coprocessor`. + +```go title="Example coprocessor service written in Go" +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +type CoprocessorRequest struct { + Version int `json:"version"` + Stage string `json:"stage"` + Headers map[string]string `json:"headers,omitempty"` + Context map[string]interface{} `json:"context,omitempty"` +} + +type CoprocessorResponse struct { + Version int `json:"version"` + Control interface{} `json:"control"` + Headers map[string]string `json:"headers,omitempty"` + Body interface{} `json:"body,omitempty"` + Context map[string]interface{} `json:"context,omitempty"` +} + +func main() { + http.HandleFunc("/coprocessor", handleCoprocessor) + + log.Println("Coprocessor running on http://127.0.0.1:8081/coprocessor") + log.Fatal(http.ListenAndServe("127.0.0.1:8081", nil)) +} + +func handleCoprocessor(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + var payload CoprocessorRequest + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + writeJSON(w, CoprocessorResponse{ + Version: 1, + Control: "continue", + }) + return + } + + log.Printf("Received coprocessor stage: %s", payload.Stage) + + writeJSON(w, CoprocessorResponse{ + Version: 1, + Control: "continue", + }) +} + +func writeJSON(w http.ResponseWriter, response CoprocessorResponse) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(http.StatusOK) + + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("failed to write response: %v", err) + } +} +``` + +The example coprocessor handles the `router.request` stage. It checks whether the incoming request (inbound request to Hive Router instance) includes an `Authorization` header. If the header is present, it adds `"auth.checked": true` to the `context`. If not, it stops the request early with a `401 Unauthorized` response. + + + + +### Configure Hive Router to call your service + +Right now, Hive Router is not configured to send anything to the coprocessor. +Next step is to add `coprocessor` to the config file: + +```yaml title="router.config.yaml" +supergraph: + source: file + path: ./supergraph.graphql + +coprocessor: + url: http://127.0.0.1:8081/coprocessor + protocol: http1 + timeout: 1s + stages: + router: + request: + include: + headers: true +``` + +This enables one stage (`router.request`) and sends `headers` to your service. + + + To modify `context`, you don't have to set `context: true` in `include`. The + same applies to `headers` or `body`. + + + + + +### Run Router and validate the call path + +Start Hive Router and send any GraphQL request. + +If everything is connected, your coprocessor service should receive one JSON payload for each +request that reaches `router.request`. + + + + +### Add an early auth gate with `control.break` + +Now implement a simple policy in your service: + +- if `authorization` header exists, return `continue` and add `"auth.checked": true` to the context. +- if it is missing, return `break` + +Example break response: + +```json title="Example response" +{ + "version": 1, + "control": { "break": 401 }, + "headers": { + "content-type": "application/json" + }, + "body": { + "errors": [{ "message": "Unauthorized" }] + } +} +``` + +```go title="Example coprocessor service written in Go" +func handleCoprocessor(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + var payload CoprocessorRequest + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + writeJSON(w, CoprocessorResponse{ + Version: 1, + Control: "continue", + }) + return + } + + log.Printf("Received coprocessor stage: %s", payload.Stage) + + // Short-circuit with 403 Unauthorized when "Authorization" header is not set. // [!code ++] + if payload.Headers["authorization"] == "" { // [!code ++] + writeJSON(w, CoprocessorResponse{ // [!code ++] + Version: 1, // [!code ++] + Control: map[string]int{ // [!code ++] + "break": http.StatusUnauthorized, // [!code ++] + }, // [!code ++] + Headers: map[string]string{ // [!code ++] + "content-type": "application/json", // [!code ++] + }, // [!code ++] + Body: map[string]interface{}{ // [!code ++] + "errors": []map[string]string{ // [!code ++] + {"message": "Unauthorized"}, // [!code ++] + }, // [!code ++] + }, // [!code ++] + }) // [!code ++] + return // [!code ++] + } // [!code ++] + + // Otherwise insert `"auth.checked": true` to context. // [!code ++] + writeJSON(w, CoprocessorResponse{ + Version: 1, + Control: "continue", + Context: map[string]interface{}{ // [!code ++] + "auth.checked": true, // [!code ++] + }, // [!code ++] + }) +} +``` + +This stops the pipeline and returns your response to the client immediately. + + + + +## Operate it safely in production + +Because coprocessors are in the request path, treat them like any critical service: + +- set a strict timeout +- monitor request rate, latency, and error rate per stage +- use traces and structured logs to debug failures quickly diff --git a/packages/documentation/content/docs/router/customizations/index.mdx b/packages/documentation/content/docs/router/customizations/index.mdx new file mode 100644 index 00000000..14fefa8e --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/index.mdx @@ -0,0 +1,51 @@ +--- +title: Router Customizations +sidebarTitle: Overview +--- + +Hive Router has two main ways to customize behavior: the [Plugins](/docs/router/customizations/plugin-system) and [Coprocessors](/docs/router/customizations/coprocessors). + +Both options can intercept requests and responses, apply changes, and integrate with rest of your architecture. +The right choice depends on your performance needs, deployment model, and team requirements. + +## Plugins + +Plugins runs inside the router process, are written in Rust and compiled into the router binary. + +Use the Plugin System when: + +- you need the lowest possible latency +- you need deep in-process integration with router internals +- your team is comfortable with Rust build and release workflows + +Plugin-based customization is a strong fit for high-throughput environments where every microsecond +matters and runtime network hops should be minimized. + + + + Understand how plugins work, what they can customize, and when to use them. + + + +## Coprocessors + +Coprocessors run as an external HTTP service called by Hive Router at configured lifecycle stages. + +Use Coprocessors when: + +- you want language-agnostic customization +- you want faster iteration without shipping a custom router binary +- you want to keep policy and transformation logic in a separate service + + + + Learn how coprocessors integrate with Hive Router, what they can control, + and where they run. + + diff --git a/packages/documentation/content/docs/router/customizations/meta.json b/packages/documentation/content/docs/router/customizations/meta.json new file mode 100644 index 00000000..2177561c --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Customizations", + "pages": ["index", "plugin-system", "coprocessors"] +} diff --git a/packages/documentation/content/docs/router/plugin-system/execution-and-lifecycle.mdx b/packages/documentation/content/docs/router/customizations/plugin-system/execution-and-lifecycle.mdx similarity index 96% rename from packages/documentation/content/docs/router/plugin-system/execution-and-lifecycle.mdx rename to packages/documentation/content/docs/router/customizations/plugin-system/execution-and-lifecycle.mdx index 108703d7..548adfd2 100644 --- a/packages/documentation/content/docs/router/plugin-system/execution-and-lifecycle.mdx +++ b/packages/documentation/content/docs/router/customizations/plugin-system/execution-and-lifecycle.mdx @@ -1,5 +1,6 @@ --- title: "Plugin Execution and Lifecycle" +sidebarTitle: "Execution and Lifecycle" --- ## Control Flow @@ -124,7 +125,7 @@ As a plugin developer, you don't need to deal with threads or async runtimes - y Router core runtime to register and manage the lifecycle of your background tasks. Background tasks can be registered during the -[`on_plugin_init` hook](/docs/router/plugin-system/hooks#on_plugin_init). Here is a simple example +[`on_plugin_init` hook](/docs/router/customizations/plugin-system/hooks#on_plugin_init). Here is a simple example of a background task that runs periodically: ```rust @@ -201,7 +202,7 @@ fn on_http_request<'req>( ### OpenTelemetry Traces -Hive Router [supports OpenTelemetry at its core](../observability/tracing), and uses the +Hive Router [supports OpenTelemetry at its core](../../observability/tracing), and uses the [`tracing` crate](https://docs.rs/tracing/latest/tracing/) for collecting, processing, and exporting OpenTelemetry trace information. diff --git a/packages/documentation/content/docs/router/plugin-system/hooks.mdx b/packages/documentation/content/docs/router/customizations/plugin-system/hooks.mdx similarity index 99% rename from packages/documentation/content/docs/router/plugin-system/hooks.mdx rename to packages/documentation/content/docs/router/customizations/plugin-system/hooks.mdx index 101b49ac..cee737cd 100644 --- a/packages/documentation/content/docs/router/plugin-system/hooks.mdx +++ b/packages/documentation/content/docs/router/customizations/plugin-system/hooks.mdx @@ -1,5 +1,6 @@ --- title: "Plugin System Hooks" +sidebarTitle: "Hooks" --- The following chart describes the execution order and available hooks. Each hook can have a diff --git a/packages/documentation/content/docs/router/plugin-system/index.mdx b/packages/documentation/content/docs/router/customizations/plugin-system/index.mdx similarity index 94% rename from packages/documentation/content/docs/router/plugin-system/index.mdx rename to packages/documentation/content/docs/router/customizations/plugin-system/index.mdx index 4e98e95c..c3907cc7 100644 --- a/packages/documentation/content/docs/router/plugin-system/index.mdx +++ b/packages/documentation/content/docs/router/customizations/plugin-system/index.mdx @@ -1,5 +1,6 @@ --- -title: "Plugin System" +title: "Introduction to Plugin System" +sidebarTitle: "Introduction" --- ## Overview @@ -16,7 +17,7 @@ parts of the service. This page documents the API, hooks, and different implementation options that are available for plugin developers. If you are looking for a guide on how to write, test and distribute a custom -plugin, please refer to [Extending the Router guide](/docs/router/guides/extending-the-router). +plugin, please refer to [Extending the Router guide](/docs/router/customizations). ## `hive-router` Crate @@ -70,7 +71,7 @@ The `RouterPlugin` trait accepts a generic type parameter for plugin configurati The configuration type must implement `DeserializeOwned + Sync`, as the Router is responsible for loading and deserializing the configuration from the Router config file. -The [`on_plugin_init`](/docs/router/plugin-system/hooks#on_plugin_init) hook provides access to the +The [`on_plugin_init`](/docs/router/customizations/plugin-system/hooks#on_plugin_init) hook provides access to the plugin's configuration. ```rust @@ -130,7 +131,7 @@ As a plugin developer, you can hook into the router execution flow in the follow - Supergraph reload notification Refer to the -[Plugin System Hooks page for the complete API and usage examples of each hook](/docs/router/plugin-system/hooks) +[Plugin System Hooks page for the complete API and usage examples of each hook](/docs/router/customizations/plugin-system/hooks) ### Shared State @@ -267,6 +268,6 @@ async fn main() -> Result<(), RouterInitError> { ## Further Reading -- [Getting Started Guide](/docs/router/guides/extending-the-router) -- [Plugin Hooks](/docs/router/plugin-system/hooks) -- [Plugin Execution and Lifecycle](/docs/router/plugin-system/execution-and-lifecycle) +- [Getting Started Guide](/docs/router/customizations) +- [Plugin Hooks](/docs/router/customizations/plugin-system/hooks) +- [Plugin Execution and Lifecycle](/docs/router/customizations/plugin-system/execution-and-lifecycle) diff --git a/packages/documentation/content/docs/router/customizations/plugin-system/meta.json b/packages/documentation/content/docs/router/customizations/plugin-system/meta.json new file mode 100644 index 00000000..8a706115 --- /dev/null +++ b/packages/documentation/content/docs/router/customizations/plugin-system/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Plugin System", + "pages": ["index", "usage", "hooks", "execution-and-lifecycle"] +} diff --git a/packages/documentation/content/docs/router/guides/extending-the-router.mdx b/packages/documentation/content/docs/router/customizations/plugin-system/usage.mdx similarity index 95% rename from packages/documentation/content/docs/router/guides/extending-the-router.mdx rename to packages/documentation/content/docs/router/customizations/plugin-system/usage.mdx index 5b9b6b94..3762f5ea 100644 --- a/packages/documentation/content/docs/router/guides/extending-the-router.mdx +++ b/packages/documentation/content/docs/router/customizations/plugin-system/usage.mdx @@ -1,5 +1,6 @@ --- -title: "Extending the Router" +title: "Writing a Plugin" +sidebarTitle: "Usage" --- import { Callout } from "@hive/design-system/hive-components/callout"; @@ -7,10 +8,10 @@ import { Step, Steps } from "fumadocs-ui/components/steps"; Hive Router is designed to be flexible and extensible, allowing you to customize its behavior to fit your specific needs. This guide demonstrates how to extend the router's functionality using -[custom plugins written in Rust](../plugin-system/index.mdx). +[custom plugins written in Rust](./index.mdx). For technical details and API reference, see the -[Router Plugin System documentation](../plugin-system/index.mdx). +[Router Plugin System documentation](./index.mdx). ## Creating a Custom Plugin @@ -131,7 +132,7 @@ interactive GraphQL playground. ### Create a custom plugin Now you can create a custom plugin by implementing the -[`RouterPlugin` trait](/docs/router/plugin-system#trait-routerplugin). +[`RouterPlugin` trait](/docs/router/customizations/plugin-system#trait-routerplugin). Then, create a `src/plugin.rs` file with the following template: @@ -172,7 +173,7 @@ impl RouterPlugin for MyPlugin { } ``` -The plugin above uses the [Plugin System Hooks API](/docs/router/plugin-system/hooks) and hooks into +The plugin above uses the [Plugin System Hooks API](/docs/router/customizations/plugin-system/hooks) and hooks into the `on_graphql_params` phase to print the received GraphQL operation to the log. @@ -332,6 +333,6 @@ docker run \ The following links and examples can help you implement custom plugins and extend the Router in different ways. -- [Plugin System documentation and API Reference](/docs/router/plugin-system) +- [Plugin System documentation and API Reference](/docs/router/customizations/plugin-system) - [Example plugins](https://github.com/graphql-hive/router/tree/main/plugin_examples) - [Plugin template](https://github.com/graphql-hive/router/blob/main/plugin_examples/plugin_template/src/plugin.rs) diff --git a/packages/documentation/content/docs/router/guides/meta.json b/packages/documentation/content/docs/router/guides/meta.json index 89be9137..167cfd15 100644 --- a/packages/documentation/content/docs/router/guides/meta.json +++ b/packages/documentation/content/docs/router/guides/meta.json @@ -2,7 +2,6 @@ "pages": [ "dynamic-subgraph-routing", "header-manipulation", - "performance-tuning", - "extending-the-router" + "performance-tuning" ] } diff --git a/packages/documentation/content/docs/router/index.mdx b/packages/documentation/content/docs/router/index.mdx index c85bd7a8..9004412e 100644 --- a/packages/documentation/content/docs/router/index.mdx +++ b/packages/documentation/content/docs/router/index.mdx @@ -49,7 +49,15 @@ lifecycle. Plugins can intercept HTTP requests, modify GraphQL operations, hook planner, and much more - all with the performance and safety guarantees of Rust. Learn how to build and integrate your own plugin: -**[Plugin System](/docs/router/plugin-system)**. +**[Plugin System](/docs/router/customizations/plugin-system)**. + +## Coprocessors + +Hive Router also supports coprocessors, an external service interface for request-time +customization. Coprocessors are language-agnostic and can inspect, mutate, or short-circuit +requests at defined stages of the router lifecycle. + +Learn more: **[Coprocessors](/docs/router/coprocessors)**. ## Subscriptions @@ -86,7 +94,7 @@ Learn more: **[OpenTelemetry Tracing](/docs/router/observability/tracing)** and - **[Getting Started](/docs/router/getting-started)** - Install and run the router in just a few minutes. -- **[Plugin System](/docs/router/plugin-system)** - Extend the router with custom plugins written in +- **[Plugin System](/docs/router/customizations/plugin-system)** - Extend the router with custom plugins written in Rust to add new features, modify behavior, or integrate with external services. - **[Subscriptions](/docs/router/subscriptions)** - Enable real-time data updates with Federated GraphQL subscriptions. diff --git a/packages/documentation/content/docs/router/meta.json b/packages/documentation/content/docs/router/meta.json index 6404fcc0..ca9df5b2 100644 --- a/packages/documentation/content/docs/router/meta.json +++ b/packages/documentation/content/docs/router/meta.json @@ -8,10 +8,10 @@ "getting-started", "configuration", "guides", + "customizations", "observability", "security", "supergraph", - "subscriptions", - "plugin-system" + "subscriptions" ] } diff --git a/packages/documentation/content/docs/router/observability/metrics.mdx b/packages/documentation/content/docs/router/observability/metrics.mdx index 22468178..c2f3e0ee 100644 --- a/packages/documentation/content/docs/router/observability/metrics.mdx +++ b/packages/documentation/content/docs/router/observability/metrics.mdx @@ -656,6 +656,68 @@ HTTP client metrics capture outbound requests the router makes to subgraphs. ]} /> +### Coprocessor + +Coprocessor metrics capture outbound calls that Hive Router makes to your coprocessor service. + +These metrics are stage-aware and help you understand where time is spent and where failures happen +in the coprocessor lifecycle. + + + +Hive Router uses an HTTP/UDS client for coprocessor calls. Because of that, coprocessor traffic is +also visible through standard HTTP client metrics, including status codes and payload sizes. + +In practice, monitor these together with coprocessor metrics: + +- `http.client.request.duration` +- `http.client.request.body.size` +- `http.client.response.body.size` +- `http.client.active_requests` + +Use labels like `http.response.status_code`, `server.address`, and `error.type` to understand +transport-level behavior (for example non-2xx responses, network failures, or slow endpoints). + +For Unix domain sockets, you still get the same HTTP-level observability view on request/response +behavior, even though transport is local socket-based. + ### Cache Cache metrics track lookup behavior and cache size across router caches used during request diff --git a/packages/documentation/content/docs/router/plugin-system/meta.json b/packages/documentation/content/docs/router/plugin-system/meta.json deleted file mode 100644 index 2fe476d7..00000000 --- a/packages/documentation/content/docs/router/plugin-system/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Plugin System", - "pages": ["index", "hooks", "execution-and-lifecycle"] -} diff --git a/packages/documentation/content/docs/router/subscriptions/websockets.mdx b/packages/documentation/content/docs/router/subscriptions/websockets.mdx index 7bedd55f..7c1fb1f4 100644 --- a/packages/documentation/content/docs/router/subscriptions/websockets.mdx +++ b/packages/documentation/content/docs/router/subscriptions/websockets.mdx @@ -50,7 +50,7 @@ Client WebSocket support is configured at the root level under `websocket` - it ### Synthetic HTTP Requests -Each WebSocket connection from a client is long-lived and can carry multiple GraphQL operations. For every operation sent over the connection - including single-shot operations like queries and mutations - the router creates a **synthetic HTTP request**. This means every operation goes through the full [plugin system](/docs/router/plugin-system) exactly as if it were a regular HTTP request. Authorization, header manipulation, rate limiting, and all other plugins apply uniformly regardless of whether the client connected over HTTP or WebSocket. +Each WebSocket connection from a client is long-lived and can carry multiple GraphQL operations. For every operation sent over the connection - including single-shot operations like queries and mutations - the router creates a **synthetic HTTP request**. This means every operation goes through the full [plugin system](/docs/router/customizations/plugin-system) exactly as if it were a regular HTTP request. Authorization, header manipulation, rate limiting, and all other plugins apply uniformly regardless of whether the client connected over HTTP or WebSocket. Because operations are processed as synthetic HTTP requests, they share the same fingerprint space as regular HTTP requests. A query sent over WebSocket can deduplicate with the same query sent over HTTP, and a WebSocket subscription can deduplicate with the same subscription started over SSE (when the `Accept` header is excluded from the fingerprint). See [Inbound Deduplication](/docs/router/guides/performance-tuning#inbound-deduplication) for details. diff --git a/packages/documentation/content/docs/schema-registry/self-hosting/changelog.mdx b/packages/documentation/content/docs/schema-registry/self-hosting/changelog.mdx index 0f1e3db5..b79ed71b 100644 --- a/packages/documentation/content/docs/schema-registry/self-hosting/changelog.mdx +++ b/packages/documentation/content/docs/schema-registry/self-hosting/changelog.mdx @@ -1,6 +1,5 @@ --- title: Self-hosting Changelog -sidebarTitle: Changelog --- import { DeploymentChangelog } from "@/components/deployment-changelog"; diff --git a/packages/documentation/content/product-updates/2026-03-05-hive-router-plugin-system/index.mdx b/packages/documentation/content/product-updates/2026-03-05-hive-router-plugin-system/index.mdx index 0b522d45..08377033 100644 --- a/packages/documentation/content/product-updates/2026-03-05-hive-router-plugin-system/index.mdx +++ b/packages/documentation/content/product-updates/2026-03-05-hive-router-plugin-system/index.mdx @@ -103,7 +103,7 @@ Plugins can intercept any stage of the request and router lifecycle: ## Learn more -- [Plugin System documentation](/docs/router/plugin-system) -- [Available hooks reference](/docs/router/plugin-system/hooks) -- [Extending the Router guide](/docs/router/guides/extending-the-router) +- [Plugin System documentation](/docs/router/customizations/plugin-system) +- [Available hooks reference](/docs/router/customizations/plugin-system/hooks) +- [Extending the Router guide](/docs/router/customizations) - [`hive-router` on Crates.io](https://crates.io/crates/hive-router) diff --git a/packages/documentation/content/product-updates/2026-04-29-hive-router-coprocessors/index.mdx b/packages/documentation/content/product-updates/2026-04-29-hive-router-coprocessors/index.mdx new file mode 100644 index 00000000..50e6545f --- /dev/null +++ b/packages/documentation/content/product-updates/2026-04-29-hive-router-coprocessors/index.mdx @@ -0,0 +1,102 @@ +--- +title: Coprocessors in Hive Router +description: + Coprocessors are now available in Hive Router - external HTTP services that can inspect, mutate, and + short-circuit requests at multiple router and GraphQL lifecycle stages. +date: 2026-04-29 +authors: [kamil] +--- + +import { Callout } from "@hive/design-system/hive-components/callout"; + +[Hive Router](/docs/router) introduces [**coprocessors**](/docs/router/customizations/coprocessors) as a first-class mechanism for extending and customizing behavior. + +A coprocessor is an external service called by the router at selected lifecycle stages. It can +inspect request data, apply controlled mutations, and decide whether to continue or short-circuit +processing with an immediate response. + +This gives teams a way to implement policy and request logic in any language, without building a +custom router binary. + +## Common use cases + +Teams can use coprocessors to implement: + +- **Authentication and authorization** + Validate tokens early in `router.request` and reject unauthorized requests before execution. + +- **Multi-tenant routing and feature flags** + Extract tenant's id from inbound request headers and enable features dynamically in `graphql.analysis`. + +- **Centralized platform policy** + Move shared logic (billing, rate limiting, entitlements) into a single external service used by many router instances. + +## Stage-Based Customization + +Each stage runs at a different point in the pipeline, so each stage is best for a different kind of logic. + +| Stage name | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `router.request` | Runs when the HTTP request enters Hive Router.
Use this stage for early traffic checks such as auth header checks and fast request rejection. | +| `graphql.request` | Runs when GraphQL request payload is available and we know it's a GraphQL request.
Use this stage for request shaping, variable normalization, and request-level guardrails. | +| `graphql.analysis` | Runs after GraphQL parsing and validation, but before query planning and execution.
This is the stage can be used to control the progressive override feature | +| `graphql.response` | Runs after GraphQL execution returns a GraphQL response.
Use this stage for response normalization, error shaping, and redaction before final HTTP response handling. | +| `router.response` | Runs at the very end, right before the response is sent to the client. | + +## Control Flow + +When Hive Router talks to a coprocessor it expects a decision, whether to `continue` or `break` the processing pipeline. + +```json tab="Continue" +{ + "version": 1, + "control": "continue" +} +``` + +```json tab="Break" +{ + "version": 1, + "control": { "break": 401 }, + "headers": { + "content-type": "application/json" + }, + "body": { + "errors": [{ "message": "Unauthorized" }] + } +} +``` + +## Network: HTTP and UDS + +In order to ask the coprocessor for a decision, they need to communicate somehow. Coprocessor calls support both: + +- `http://` endpoints (remote/shared services) +- `unix://` endpoints (local sidecar via Unix Domain Socket) + + + For low-latency deployments, running the coprocessor close to the router and + using UDS reduces network's cost (DNS lookups etc) and improves performance. + + +## Observability + +Coprocessor services become a critical part of your production setup, so Hive Router provides metrics and traces to help you monitor performance and diagnose issues. + +Metrics: + +- `hive.router.coprocessor.requests_total` +- `hive.router.coprocessor.duration` +- `hive.router.coprocessor.errors_total` + +All three are labeled by `coprocessor.stage`. + +Tracing also includes a `coprocessor` span per stage call (with `coprocessor.stage`, `coprocessor.id` attributes) +and nested `http.client` spans for outbound coprocessor calls. + +--- + +- [Coprocessors overview](/docs/router/customizations/coprocessors) +- [Writing a coprocessor](/docs/router/customizations/coprocessors/usage) +- [Stages and protocol](/docs/router/customizations/coprocessors/stages-and-protocol) +- [`coprocessor` configuration reference](/docs/router/configuration/coprocessor) diff --git a/packages/documentation/e2e/relative-links.e2e.ts b/packages/documentation/e2e/relative-links.e2e.ts index 664b3d6c..82aa6599 100644 --- a/packages/documentation/e2e/relative-links.e2e.ts +++ b/packages/documentation/e2e/relative-links.e2e.ts @@ -6,7 +6,9 @@ test.describe("Relative link resolution", () => { page, }) => { const response = await page.goto( - appPath("/docs/router/plugin-system/execution-and-lifecycle"), + appPath( + "/docs/router/customizations/plugin-system/execution-and-lifecycle", + ), { waitUntil: "networkidle" }, ); if (!response?.ok()) { @@ -26,43 +28,37 @@ test.describe("Relative link resolution", () => { test("relative .mdx link resolves to absolute /docs/ URL", async ({ page, }) => { - const response = await page.goto( - appPath("/docs/router/guides/extending-the-router"), - { - waitUntil: "networkidle", - }, - ); + const response = await page.goto(appPath("/docs/router/customizations"), { + waitUntil: "networkidle", + }); if (!response?.ok()) { test.skip(true, "Page not available (needs build)"); } const link = page.getByRole("link", { - name: "custom plugins written in Rust", + name: "Introduction to Plugins", }); await expect(link).toBeVisible(); await expect(link).toHaveAttribute( "href", - appPath("/docs/router/plugin-system"), + appPath("/docs/router/customizations/plugin-system"), ); }); test("clicking resolved relative link navigates successfully", async ({ page, }) => { - const response = await page.goto( - appPath("/docs/router/guides/extending-the-router"), - { - waitUntil: "networkidle", - }, - ); + const response = await page.goto(appPath("/docs/router/customizations"), { + waitUntil: "networkidle", + }); if (!response?.ok()) { test.skip(true, "Page not available (needs build)"); } - await page - .getByRole("link", { name: "custom plugins written in Rust" }) - .click(); - await expect(page).toHaveURL(appPathPattern("/docs/router/plugin-system")); + await page.getByRole("link", { name: "Introduction to Plugins" }).click(); + await expect(page).toHaveURL( + appPathPattern("/docs/router/customizations/plugin-system"), + ); await expect( page.getByRole("heading", { level: 1, name: "Plugin System" }), ).toBeVisible({ timeout: 10_000 }); diff --git a/packages/documentation/package.json b/packages/documentation/package.json index ce2860eb..1030d6a3 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -57,7 +57,8 @@ "shiki": "^3.23.0", "tailwind-merge": "3.4.0", "twoslash": "0.3.6", - "vite": "7.3.1" + "vite": "7.3.1", + "zod": "4.3.6" }, "devDependencies": { "@playwright/test": "1.57.0", diff --git a/packages/documentation/redirects.ts b/packages/documentation/redirects.ts index 4bf3c7bc..2f89e3c4 100644 --- a/packages/documentation/redirects.ts +++ b/packages/documentation/redirects.ts @@ -145,6 +145,20 @@ export const routeRules: Record = { "/docs/gateway/other-features/rust-query-planner", ), + // Support pages moved after introducing coprocessors + "/docs/router/plugin-system": redirect( + "/docs/router/customizations/plugin-system", + ), + "/docs/router/plugin-system/hooks": redirect( + "/docs/router/customizations/plugin-system/hooks", + ), + "/docs/router/plugin-system/execution-and-lifecycle": redirect( + "/docs/router/customizations/plugin-system/execution-and-lifecycle", + ), + "/docs/router/guides/extending-the-router": redirect( + "/docs/router/customizations", + ), + // Typo in old URL (linked from blog posts) "/docs/schema-registry/high-availability-resilence": redirect( "/docs/schema-registry/high-availability-resilience", diff --git a/packages/documentation/source.config.ts b/packages/documentation/source.config.ts index f86a94e9..5d036995 100644 --- a/packages/documentation/source.config.ts +++ b/packages/documentation/source.config.ts @@ -1,5 +1,6 @@ import { type } from "arktype"; import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; +import { pageSchema } from "fumadocs-core/source/schema"; import { defineCollections, defineConfig, @@ -8,6 +9,7 @@ import { import lastModified from "fumadocs-mdx/plugins/last-modified"; import { transformerTwoslash } from "fumadocs-twoslash"; import rehypeMermaid, { type RehypeMermaidOptions } from "rehype-mermaid"; +import { z } from "zod"; import { DOCS_CODE_LANGS, DOCS_CODE_THEMES } from "./src/lib/docs-code-config"; import { autoImage, remarkAutoImage } from "./tools/source-plugins/auto-image"; @@ -17,6 +19,9 @@ export const docs = defineDocs({ dir: "content/docs", docs: { async: true, + schema: pageSchema.extend({ + sidebarTitle: z.string().optional(), + }), postprocess: { includeProcessedMarkdown: true, }, @@ -30,7 +35,7 @@ const author = type("string | object").pipe((v) => /** YAML parses unquoted `date: 2025-01-27` as a Date object. Accept both. */ const dateString = type("string | Date").pipe((v) => - typeof v === "string" ? v : v.toISOString().split("T")[0]!, + typeof v === "string" ? v : (v.toISOString().split("T")[0] ?? ""), ); export const caseStudies = defineCollections({ diff --git a/packages/documentation/src/lib/source.ts b/packages/documentation/src/lib/source.ts index b674c3aa..ef3aa906 100644 --- a/packages/documentation/src/lib/source.ts +++ b/packages/documentation/src/lib/source.ts @@ -53,6 +53,38 @@ function hiveIconsPlugin(): LoaderPlugin { }; } +/** + * This plugin allows to change the label of the page visible in the sidebar navigation, + * without changing the page title displayed on the tab. + * + * @example + * ``` + * --- + * title: "Title displayed on the page and the tab" + * sidebarTitle: "Title displayed in the sidebar navigation" + * --- + * ``` + **/ +function sidebarTitlePlugin(): LoaderPlugin { + return { + name: "hive:sidebarTitle", + transformStorage: ({ storage }) => { + for (const path of storage.getFiles()) { + const file = storage.read(path); + if (!file || file.format !== "page") continue; + + const pageData = file.data as PageData & { sidebarTitle?: unknown }; + if ( + typeof pageData.sidebarTitle === "string" && + pageData.sidebarTitle.length > 0 + ) { + pageData.title = pageData.sidebarTitle; + } + } + }, + }; +} + function createSource(docs: { toFumadocsSource(): unknown }) { const docsSource = docs.toFumadocsSource() as Source<{ metaData: MetaCollectionEntry; @@ -60,7 +92,7 @@ function createSource(docs: { toFumadocsSource(): unknown }) { }>; return loader({ baseUrl: "/docs", - plugins: [hiveIconsPlugin()], + plugins: [hiveIconsPlugin(), sidebarTitlePlugin()], source: docsSource, }); } diff --git a/packages/documentation/tools/source-plugins/remark-relative-links.ts b/packages/documentation/tools/source-plugins/remark-relative-links.ts index 6626f6d0..2f55c5e1 100644 --- a/packages/documentation/tools/source-plugins/remark-relative-links.ts +++ b/packages/documentation/tools/source-plugins/remark-relative-links.ts @@ -7,7 +7,7 @@ import { dirname, relative, resolve } from "node:path"; * Relative links like `[text](../foo)` or `[text](../foo.mdx)` are not * transformed by Fumadocs and break TanStack Start prerender. * This plugin resolves them against the filesystem and rewrites to - * absolute URL paths like `/docs/router/plugin-system`. + * absolute URL paths like `/docs/router/customizations/plugin-system`. */ export function remarkRelativeLinks() { return remarkRelativeLinkTransform; diff --git a/temporary/packages/web/docs/src/app/product-updates/(posts)/2026-03-05-hive-router-plugin-system/page.mdx b/temporary/packages/web/docs/src/app/product-updates/(posts)/2026-03-05-hive-router-plugin-system/page.mdx index 0b522d45..08377033 100644 --- a/temporary/packages/web/docs/src/app/product-updates/(posts)/2026-03-05-hive-router-plugin-system/page.mdx +++ b/temporary/packages/web/docs/src/app/product-updates/(posts)/2026-03-05-hive-router-plugin-system/page.mdx @@ -103,7 +103,7 @@ Plugins can intercept any stage of the request and router lifecycle: ## Learn more -- [Plugin System documentation](/docs/router/plugin-system) -- [Available hooks reference](/docs/router/plugin-system/hooks) -- [Extending the Router guide](/docs/router/guides/extending-the-router) +- [Plugin System documentation](/docs/router/customizations/plugin-system) +- [Available hooks reference](/docs/router/customizations/plugin-system/hooks) +- [Extending the Router guide](/docs/router/customizations) - [`hive-router` on Crates.io](https://crates.io/crates/hive-router) diff --git a/temporary/packages/web/docs/src/content/router/guides/extending-the-router.mdx b/temporary/packages/web/docs/src/content/router/guides/extending-the-router.mdx index f3ff83b1..f41582ff 100644 --- a/temporary/packages/web/docs/src/content/router/guides/extending-the-router.mdx +++ b/temporary/packages/web/docs/src/content/router/guides/extending-the-router.mdx @@ -117,7 +117,7 @@ interactive GraphQL playground. ### Create a custom plugin Now you can create a custom plugin by implementing the -[`RouterPlugin` trait](/docs/router/plugin-system#trait-routerplugin). +[`RouterPlugin` trait](/docs/router/customizations/plugin-system#trait-routerplugin). Then, create a `src/plugin.rs` file with the following template: @@ -158,7 +158,7 @@ impl RouterPlugin for MyPlugin { } ``` -The plugin above uses the [Plugin System Hooks API](/docs/router/plugin-system/hooks) and hooks into +The plugin above uses the [Plugin System Hooks API](/docs/router/customizations/plugin-system/hooks) and hooks into the `on_graphql_params` phase to print the received GraphQL operation to the log. @@ -302,6 +302,6 @@ docker run \ The following links and examples can help you implement custom plugins and extend the Router in different ways. -- [Plugin System documentation and API Reference](/docs/router/plugin-system) +- [Plugin System documentation and API Reference](/docs/router/customizations/plugin-system) - [Example plugins](https://github.com/graphql-hive/router/tree/main/plugin_examples) - [Plugin template](https://github.com/graphql-hive/router/blob/main/plugin_examples/plugin_template/src/plugin.rs) diff --git a/temporary/packages/web/docs/src/content/router/plugin-system/execution-and-lifecycle.mdx b/temporary/packages/web/docs/src/content/router/plugin-system/execution-and-lifecycle.mdx index 20d49dbe..50166770 100644 --- a/temporary/packages/web/docs/src/content/router/plugin-system/execution-and-lifecycle.mdx +++ b/temporary/packages/web/docs/src/content/router/plugin-system/execution-and-lifecycle.mdx @@ -124,7 +124,7 @@ As a plugin developer, you don't need to deal with threads or async runtimes - y Router core runtime to register and manage the lifecycle of your background tasks. Background tasks can be registered during the -[`on_plugin_init` hook](/docs/router/plugin-system/hooks#on_plugin_init). Here is a simple example +[`on_plugin_init` hook](/docs/router/customizations/plugin-system/hooks#on_plugin_init). Here is a simple example of a background task that runs periodically: ```rust diff --git a/temporary/packages/web/docs/src/content/router/plugin-system/index.mdx b/temporary/packages/web/docs/src/content/router/plugin-system/index.mdx index 07e81e81..1962dcc4 100644 --- a/temporary/packages/web/docs/src/content/router/plugin-system/index.mdx +++ b/temporary/packages/web/docs/src/content/router/plugin-system/index.mdx @@ -18,7 +18,7 @@ parts of the service. This page documents the API, hooks, and different implementation options that are available for plugin developers. If you are looking for a guide on how to write, test and distribute a custom -plugin, please refer to [Extending the Router guide](/docs/router/guides/extending-the-router). +plugin, please refer to [Extending the Router guide](/docs/router/customizations). ## `hive-router` Crate @@ -72,7 +72,7 @@ The `RouterPlugin` trait accepts a generic type parameter for plugin configurati The configuration type must implement `DeserializeOwned + Sync`, as the Router is responsible for loading and deserializing the configuration from the Router config file. -The [`on_plugin_init`](/docs/router/plugin-system/hooks#on_plugin_init) hook provides access to the +The [`on_plugin_init`](/docs/router/customizations/plugin-system/hooks#on_plugin_init) hook provides access to the plugin's configuration. ```rust @@ -132,7 +132,7 @@ As a plugin developer, you can hook into the router execution flow in the follow - Supergraph reload notification Refer to the -[Plugin System Hooks page for the complete API and usage examples of each hook](/docs/router/plugin-system/hooks) +[Plugin System Hooks page for the complete API and usage examples of each hook](/docs/router/customizations/plugin-system/hooks) ### Shared State @@ -269,6 +269,6 @@ async fn main() -> Result<(), RouterInitError> { ## Further Reading -- [Getting Started Guide](/docs/router/guides/extending-the-router) -- [Plugin Hooks](/docs/router/plugin-system/hooks) -- [Plugin Execution and Lifecycle](/docs/router/plugin-system/execution-and-lifecycle) +- [Getting Started Guide](/docs/router/customizations) +- [Plugin Hooks](/docs/router/customizations/plugin-system/hooks) +- [Plugin Execution and Lifecycle](/docs/router/customizations/plugin-system/execution-and-lifecycle)