Self-hosted Gateway

The Grafbase Gateway allows you to run a federated graph within your own infrastructure. The gateway operates by either polling the latest graph from the Grafbase API or by passing the federated schema, enabling air-gapped operation.

To install the Grafbase Gateway, run the following command:

curl -fsSL https://grafbase.com/downloads/gateway | bash

In hybrid mode, the gateway fetches the current federated graph from the Grafbase platform. Create a federated graph in the Grafbase API, publish the subgraphs, and the gateway will always have the current graph running.

Start the gateway in hybrid mode with the graph reference and an organization access token:

GRAFBASE_ACCESS_TOKEN=token ./grafbase-gateway \ --config grafbase.toml \ --graph-ref graph@branch

graph-ref points to a graph created in the Grafbase API and its branch. If the branch is empty, the gateway uses the production branch by default.

Create the organization access token in the account settings under "Access Tokens" and ensure it has permission to read the graph.

The gateway polls for graph changes every ten seconds.

In air-gapped mode, the gateway never calls the Grafbase API. You must provide the federated graph SDL as a file.

Start the gateway in self-hosted mode:

./grafbase-gateway \ --config /path/to/grafbase.toml \ --schema /path/to/federated-schema.graphql \ --listen-address 127.0.0.1:4000

Define the configuration using the TOML file format.

The gateway loads the configuration at startup, and you can hot-reload parts of it during runtime with the --hot-reload flag. The gateway polls for configuration changes every two seconds when provided a path to the configuration file.

[graph] path = "/graphql" introspection = false
  • path: The URL path where the GraphQL API is available. Defaults to /graphql.
  • introspection: Enables or disables GraphQL introspection. Defaults to false.
[network] listen_address = "127.0.0.1:5000"
  • listen_address: The address to bind to. Defaults to 127.0.0.1:5000. Can be set in the configuration or via the command line interface using the --listen-address argument.

Set gateway server settings in this section:

[gateway] timeout = "30s" subgraph_timeout = "4s"
  • timeout: Timeout for slow requests and responses. Default: 30s.
  • subgraph_timeout: A global timeout for all subgraph requests. A subgraph can override this setting.

Read more about rate limiting.

[gateway.rate_limit.global] limit = 100 duration = "10s"

You can hot-reload the subgraph rate limit and duration settings.

Read more about rate limit specific for a subgraph.

The Grafbase Gateway requires configuration to enable query batching. Be aware that a large batch of queries can trigger a denial of service attack on the subgraph service or the gateway itself.

[gateway.batching] enabled = true limit = 5
  • enabled: Enables query batching. Defaults to false.
  • limit: The maximum number of queries in a batch. If not set, the gateway does not limit the number of queries in a batch.

The retry configuration lets you define how a failing subgraph request gets handled. The subgraph request can fail if the subgraph service times out, returns an error code, or the rate limit is reached.

[gateway.retry] enabled = true min_per_second = 10 ttl = "1s" retry_percent = 0.1 retry_mutations = false

The retries are based on a budget logic. Each successful request to the subgraph stores a deposit to the budget, and each failing request withdraws from the budget.

  • enabled: Enables retries for the given subgraph. Defaults to false.
  • min_per_second: How many retries are available per second, at a minimum. Defaults to 10.
  • ttl: Each successful request to the subgraph adds to the retry budget. This setting controls how long the budget remembers successful requests. Defaults to 10s.
  • retry_percent: The fraction of the successful requests budget that can be used for retries. Defaults to 0.2.
  • retry_mutations: Whether mutations should be retried at all. Enable this setting only if mutations are idempotent. Defaults to false.

If you enable subgraph retries, they will execute with an exponential backoff. The first retry occurs after 100 milliseconds, the second after 200 milliseconds, the third after 400 milliseconds, and so on. The engine will add jitter to the times to prevent the thundering herd problem of having too many requests reaching the subgraph at once. A jitter multiplier between 0.0 to 2.0 is added to the retry backoff.

Read more on retries specific for a subgraph.

Read more about access logs to implement the required hooks.

[gateway.access_logs] enabled = true path = "/path/to/logs" rotate = "daily" mode = "blocking"
  • enabled: Enables the access log writer.
  • path: Specifies the log directory.
  • rotate: Defines a rotation strategy, after which a new log file is created, and the previous one gets archived; options include never, minutely, hourly, or daily.
  • rotate.size: Defines a size-based rotation strategy. The value is in bytes; the log file rotates when it reaches or exceeds this limit.
  • mode: Specifies how the system behaves when the log queue is full and the writer can't write to the file quickly enough. Options include blocking, which blocks the caller, or non_blocking, which returns an error while sending the data back to the caller.

The current log filename will be access.log, saved to the specified path. If you enable rotation, the current log file is access.log, and archived files will include a timestamp suffix. The timestamp is from the moment the logger started writing to the file.

Enable CSRF protection if the graph is accessible over the internet with a browser.

If enabled, you must provide a special header x-grafbase-csrf-protection: 1 in every request not OPTIONS. The server returns 403 Forbidden if the header is not found.

[csrf] enabled = true
  • enabled: Enables CSRF protection. Defaults to false.

Configure CORS to prevent unauthorized browser requests.

[cors] allow_credentials = false allow_origins = ["https://app.grafbase.com"] max_age = "60s" allow_methods = ["GET", "POST"] allow_headers = ["Content-Type"] expose_headers = ["Access-Control-Allow-Origin"] allow_private_network = false
  • allow_credentials: Enables or disables credential sending. Defaults to false.
  • allow_origins: List of allowed domains or “any”. Defaults to no domains if CORS is enabled.
  • max_age: Duration for caching preflight OPTIONS request results. Default: none.
  • allow_methods: List of allowed HTTP methods or “any”. Defaults to none if CORS is enabled.
  • allow_headers: List of allowed headers or “any”. Defaults to no headers if CORS is enabled.
  • expose_headers: Headers a preflight request can return to the client. Default: no headers if CORS is enabled.
  • allow_private_network: Allows private network requests. Defaults to false.
[tls] certificate = "/path/to/cert.pem" key = "/path/to/key.pem"

The gateway can serve HTTPS traffic without a reverse proxy. If no tls section is defined, the server operates in plain HTTP mode.

  • certificate: Path to the certificate file in PEM format.
  • key: Path to the private key file in PEM format.
[operation_limits] depth = 10 height = 12 aliases = 5 root_fields = 6 complexity = 2

Read more about Operation Limits.

[trusted_documents] enabled = true bypass_header_name = "my-header-name" bypass_header_value = "my-secret-value"

Read more about Trusted Documents.

The gateway supports JWT authentication, configurable through the TOML file:

[authentication.providers.jwt] name = "my-authenticator" [authentication.providers.jwt.jwks] url = "https://example.com/.well-known/jwks.json" issuer = "example.com" audience = "my-project" poll_interval = 60 [authentication.providers.jwt.header] name = "Authorization" value_prefix = "Bearer "

Read more about Federated Authorization.

Define headers as rules executed in order. Use forward, insert, rename_duplicate or remove to manage headers sent to subgraphs:

[[headers]] rule = "forward" name = "authorization" [[headers]] rule = "forward" name = "x-custom-header" rename = "y-custom-header" [[headers]] rule = "forward" name = "x-possible-empty" default = "default-value"

You can forward multiple headers using a regular expression.

The headers the engine is matching against are in lowercase, so either write the regular expression all in lowercase, or provide a flag in the expression to match lower and uppercase letters.

[[headers]] rule = "forward" pattern = "^x-custom*"

Insert header values with the insert rule:

[[headers]] rule = "insert" name = "authorization" value = "Bearer secret-token"

You can insert headers values from environment variables:

[[headers]] rule = "insert" name = "authorization" value = "Bearer {{ env.AUTHORIZATION_TOKEN }}"

Use the remove rule to prevent forwarding of selected headers when using patterns:

[[headers]] rule = "remove" name = "x-custom-secret"

The rename_duplicate rule is to forward the defined header value and a copy of the value with the given rename to the subgraphs.

[[headers]] rule = "rename_duplicate" name = "x-custom-value" default = "the value was missing" rename = "y-custom-value"

This will forward two headers to the subgraph, both with the same value. One with the name x-custom-value and the other with the name y-custom-value. If the header x-custom-value is missing in the request, by defining the default the gateway will create two headers with the given default. If the default is omitted, the gateway will not forward the header or create a duplicate.

The following headers cannot be controlled through header rules:

  • Accept
  • Accept-Charset
  • Accept-Encoding
  • Accept-Ranges
  • Connection
  • Content-Length
  • Content-Type
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade

Read more on subgraph header rules.

Enable the gateway to expose a health check endpoint:

[health] enabled = true listen = "0.0.0.0:9668" path = "/health"
  • enabled: Whether to expose the health check endpoint. Defaults to true.
  • path: The request path for the endpoint. Defaults to "/health".
  • listen: Address and port to bind for health requests. If omitted, uses the main GraphQL endpoint's socket and port.

The health check endpoint returns a status code 200 with with a body of {"status": "healthy"} when the gateway is in a healthy state. It will reply with a status code 503 when it can determine that the gateway is not healthy.

The TLS configuration from tls in your configuration also applies to the health check endpoint.

You can define settings per subgraph. These settings will either add to or override the global settings.

Define per-subgraph options such as the WebSocket URL and timeouts:

[subgraphs.products] websocket_url = "https://example.com/" timeout = "4s"

You can override the URL for a subgraph. If set, the gateway will use the provided URL instead of the one from the schema registry.

[subgraphs.products] url = "https://example.com/graphql"

Define header rules per subgraph. They execute after the global rules.

[[subgraphs.products.headers]] rule = "forward" name = "content-type"

Read more on global header rules.

You can define rate limit per subgraph, which is checked before issuing a request. Read more about rate limiting.

[subgraphs.products.rate_limit] limit = 100 duration = "10s"

You can hot-reload the subgraph rate limit and duration settings.

A rate limit global rate limit.

You can override the retry budget for the specific subgraph.

[subgraphs.products.retry] enabled = true min_per_second = 10 ttl = "1s" retry_percent = 0.1 retry_mutations = false

Read more about retries.

The server is written in Rust and consists of several components:

  • axum web server
  • Tokio runtime for async operations, running one worker per CPU core
  • Rustls for encryption, backed by the ring and BoringTLS libraries
  • Reqwest HTTP client, using Hyper and Rustls for encryption

In hybrid mode, the gateway validates the access token and fetches the latest federated graph from the CDN every ten seconds. It updates the graph and replaces the GraphQL engine as needed.

Example compose.yaml:

version: '3' services: grafbase: image: ghcr.io/grafbase/gateway:latest restart: always volumes: - ./grafbase.toml:/etc/grafbase.toml environment: GRAFBASE_GRAPH_REF: 'graph-ref@branch' GRAFBASE_ACCESS_TOKEN: 'ACCESS_TOKEN_HERE' ports: - '5000:5000'