Skip to content

Federation Configuration

FraiseQL federation means running multiple independent FraiseQL services, each with its own database and compiled schema, composed into a supergraph by a gateway. This guide covers the fraiseql.toml configuration for each service, the built-in fraiseql gateway, and the Apollo Router setup.

Federation architecture: client to gateway to subgraph services with separate databases Federation architecture: client to gateway to subgraph services with separate databases
Each service has its own database — the gateway composes them into a single endpoint.

Each FraiseQL service is entirely standalone:

  • Its own fraiseql.toml with a single [database] section
  • Its own Python schema file that compiles to schema.compiled.json
  • Its own Rust runtime process

Every FraiseQL service has exactly one database. Use the [database] section (singular, not [databases.service_name]).

user-service/fraiseql.toml
[project]
name = "user-service"
version = "1.0.0"
description = "User accounts and authentication subgraph"
[fraiseql]
schema_file = "schema.json"
output_file = "schema.compiled.json"
[database]
url = "${DATABASE_URL}"
[federation]
enabled = true
entity_key = "id"
[security.audit_logging]
enabled = true
log_level = "info"
async_logging = true
buffer_size = 1000
flush_interval_secs = 5
[security.error_sanitization]
enabled = true
generic_messages = true
internal_logging = true
leak_sensitive_details = false
[security.rate_limiting]
enabled = true
auth_start_max_requests = 100
auth_start_window_secs = 60
failed_login_max_requests = 5
failed_login_window_secs = 3600
[security.pkce]
client_id = "${OIDC_CLIENT_ID}"
issuer_url = "${OIDC_ISSUER_URL}"
redirect_uri = "${OIDC_REDIRECT_URI}"
order-service/fraiseql.toml
[project]
name = "order-service"
version = "1.0.0"
description = "Orders and fulfillment subgraph"
[fraiseql]
schema_file = "schema.json"
output_file = "schema.compiled.json"
[database]
url = "${DATABASE_URL}"
[federation]
enabled = true
entity_key = "id"
[security.audit_logging]
enabled = true
log_level = "info"
async_logging = true
[security.error_sanitization]
enabled = true
generic_messages = true
leak_sensitive_details = false
[observers]
backend = "nats"
nats_url = "${NATS_URL}"
[security.pkce]
client_id = "${OIDC_CLIENT_ID}"
issuer_url = "${OIDC_ISSUER_URL}"
redirect_uri = "${OIDC_REDIRECT_URI}"

These are the real fraiseql.toml keys. Do not use keys not listed here.

KeyTypeDescription
urlstringConnection URL (use environment variable reference ${VAR})
pool_minintMinimum pool connections (default: 2)
pool_maxintMaximum pool connections
connect_timeout_msintConnection timeout in milliseconds
idle_timeout_msintIdle connection timeout in milliseconds
ssl_modestring"disable", "allow", "prefer", or "require"
[database]
url = "${DATABASE_URL}"
KeyTypeDescription
schema_filestringInput: JSON generated by Python decorators
output_filestringOutput: compiled schema for the Rust runtime
KeyTypeDefaultDescription
enabledbooltrueEnable security audit logging
log_levelstring"info""debug", "info", or "warn"
include_sensitive_databoolfalseNever true in production
async_loggingbooltrueNon-blocking log writes
buffer_sizeint1000Events buffered before flush
flush_interval_secsint5Seconds between flushes
KeyTypeDefaultDescription
enabledbooltrueSanitize error messages sent to clients
generic_messagesbooltrueAlways true in production
internal_loggingbooltrueLog full details internally
leak_sensitive_detailsboolfalseNever true in production
user_facing_formatstring"generic""generic", "simple", or "detailed"
KeyTypeDescription
enabledboolEnable rate limiting
auth_start_max_requestsintMax /auth/start requests per IP per window
auth_start_window_secsintTime window in seconds
auth_callback_max_requestsintMax /auth/callback requests per IP per window
auth_callback_window_secsintTime window in seconds
auth_refresh_max_requestsintMax /auth/refresh requests per user per window
auth_refresh_window_secsintTime window in seconds
failed_login_max_requestsintMax failed login attempts before lockout
failed_login_window_secsintLockout window in seconds

OIDC/PKCE configuration. The client secret is never stored in fraiseql.toml — provide it via environment variable at runtime.

[security.pkce]
client_id = "${OIDC_CLIENT_ID}"
issuer_url = "https://your-idp.example.com"
redirect_uri = "https://your-service.example.com/auth/callback"

Configure the observer backend for receiving mutation events:

[observers]
backend = "nats" # "nats", "redis", or "postgres"
nats_url = "${NATS_URL}"
[observers]
backend = "redis"
redis_url = "${REDIS_URL}"
[federation]
enabled = true
entity_key = "id"

Secrets are never stored in fraiseql.toml. Reference them with ${VAR_NAME} syntax:

user-service/.env
DATABASE_URL=postgresql://user_svc:password@user-db:5432/users
OIDC_CLIENT_ID=user-service-client
OIDC_ISSUER_URL=https://auth.example.com
OIDC_REDIRECT_URI=https://users.example.com/auth/callback
STATE_ENCRYPTION_KEY=<base64-32-bytes>
order-service/.env
DATABASE_URL=postgresql://order_svc:password@order-db:5432/orders
NATS_URL=nats://nats:4222
OIDC_CLIENT_ID=order-service-client
OIDC_ISSUER_URL=https://auth.example.com
OIDC_REDIRECT_URI=https://orders.example.com/auth/callback
STATE_ENCRYPTION_KEY=<base64-32-bytes>

Generate encryption keys:

Terminal window
export STATE_ENCRYPTION_KEY=$(openssl rand -base64 32)

FraiseQL offers two gateway options. For FraiseQL-only subgraphs, the built-in gateway is the simplest option. For mixed subgraphs, use Apollo Router.

gateway.toml
[gateway]
port = 4000
[[gateway.subgraphs]]
name = "users"
url = "http://user-service:4001/graphql"
[[gateway.subgraphs]]
name = "orders"
url = "http://order-service:4002/graphql"
[gateway.circuit_breaker]
failure_threshold = 5
recovery_timeout_secs = 30
Terminal window
fraiseql gateway --config gateway.toml

See Federation Gateway Guide for the full configuration reference.

The Apollo Router connects to each FraiseQL subgraph by URL. It does not read fraiseql.toml files — it communicates with the running Rust runtime processes.

router.yaml
supergraph:
listen: 0.0.0.0:4000
# Forward the client's Authorization header to every subgraph
headers:
all:
request:
- propagate:
named: Authorization
subgraphs:
users:
routing_url: http://user-service:4001/graphql
orders:
routing_url: http://order-service:4002/graphql

Use the Rover CLI to compose subgraph schemas into a supergraph:

supergraph.yaml
federation_version: =2.4.0
subgraphs:
users:
routing_url: http://user-service:4001/graphql
schema:
subgraph_url: http://user-service:4001/graphql
orders:
routing_url: http://order-service:4002/graphql
schema:
subgraph_url: http://order-service:4002/graphql
Terminal window
rover supergraph compose --config supergraph.yaml > supergraph.graphql
router --config router.yaml --supergraph supergraph.graphql

fraiseql.toml (development overrides)
[security.audit_logging]
log_level = "debug"
include_sensitive_data = true # OK for local dev only
async_logging = false # Synchronous for easier debugging
[security.error_sanitization]
user_facing_format = "detailed" # Show full errors locally
[security.rate_limiting]
auth_start_max_requests = 10000
failed_login_max_requests = 10000
fraiseql.toml (production)
[security.audit_logging]
enabled = true
log_level = "info"
include_sensitive_data = false
async_logging = true
[security.error_sanitization]
enabled = true
generic_messages = true
leak_sensitive_details = false
[security.rate_limiting]
enabled = true
auth_start_max_requests = 100
failed_login_max_requests = 5
failed_login_window_secs = 3600

federation-example/
├── user-service/
│ ├── fraiseql.toml
│ ├── schema.py
│ └── Dockerfile
├── order-service/
│ ├── fraiseql.toml
│ ├── schema.py
│ └── Dockerfile
├── router.yaml
├── supergraph.yaml
└── docker-compose.yml
docker-compose.yml
services:
user-db:
image: postgres:16
environment:
POSTGRES_DB: users
POSTGRES_USER: user_svc
POSTGRES_PASSWORD: ${USER_DB_PASSWORD}
order-db:
image: postgres:16
environment:
POSTGRES_DB: orders
POSTGRES_USER: order_svc
POSTGRES_PASSWORD: ${ORDER_DB_PASSWORD}
nats:
image: nats:latest
command: ["-js"] # Enable JetStream
user-service:
build: ./user-service
environment:
DATABASE_URL: postgresql://user_svc:${USER_DB_PASSWORD}@user-db:5432/users
OIDC_CLIENT_ID: ${USER_OIDC_CLIENT_ID}
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL}
OIDC_REDIRECT_URI: ${USER_REDIRECT_URI}
STATE_ENCRYPTION_KEY: ${STATE_ENCRYPTION_KEY}
ports:
- "4001:4001"
depends_on:
- user-db
order-service:
build: ./order-service
environment:
DATABASE_URL: postgresql://order_svc:${ORDER_DB_PASSWORD}@order-db:5432/orders
NATS_URL: nats://nats:4222
OIDC_CLIENT_ID: ${ORDER_OIDC_CLIENT_ID}
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL}
OIDC_REDIRECT_URI: ${ORDER_REDIRECT_URI}
STATE_ENCRYPTION_KEY: ${STATE_ENCRYPTION_KEY}
ports:
- "4002:4002"
depends_on:
- order-db
- nats
apollo-router:
image: ghcr.io/apollographql/router:latest
volumes:
- ./router.yaml:/dist/config/router.yaml
- ./supergraph.graphql:/dist/config/supergraph.graphql
ports:
- "4000:4000"
depends_on:
- user-service
- order-service
Terminal window
# Step 1: Compile each service
cd user-service && python schema.py && fraiseql compile && cd ..
cd order-service && python schema.py && fraiseql compile && cd ..
# Step 2: Compose supergraph (requires running services)
docker-compose up user-service order-service -d
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# Step 3: Start everything
docker-compose up

Each FraiseQL subgraph in a federated deployment is a full multi-transport server. REST and gRPC annotations work in federated subgraphs — individual services can serve all three transports.