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.
Architecture
Section titled “Architecture”Each FraiseQL service is entirely standalone:
- Its own
fraiseql.tomlwith a single[database]section - Its own Python schema file that compiles to
schema.compiled.json - Its own Rust runtime process
Per-Service fraiseql.toml
Section titled “Per-Service fraiseql.toml”Every FraiseQL service has exactly one database. Use the [database] section (singular, not [databases.service_name]).
User Service
Section titled “User Service”[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 = trueentity_key = "id"
[security.audit_logging]enabled = truelog_level = "info"async_logging = truebuffer_size = 1000flush_interval_secs = 5
[security.error_sanitization]enabled = truegeneric_messages = trueinternal_logging = trueleak_sensitive_details = false
[security.rate_limiting]enabled = trueauth_start_max_requests = 100auth_start_window_secs = 60failed_login_max_requests = 5failed_login_window_secs = 3600
[security.pkce]client_id = "${OIDC_CLIENT_ID}"issuer_url = "${OIDC_ISSUER_URL}"redirect_uri = "${OIDC_REDIRECT_URI}"Order Service
Section titled “Order Service”[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 = trueentity_key = "id"
[security.audit_logging]enabled = truelog_level = "info"async_logging = true
[security.error_sanitization]enabled = truegeneric_messages = trueleak_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}"TOML Key Reference
Section titled “TOML Key Reference”These are the real fraiseql.toml keys. Do not use keys not listed here.
[database]
Section titled “[database]”| Key | Type | Description |
|---|---|---|
url | string | Connection URL (use environment variable reference ${VAR}) |
pool_min | int | Minimum pool connections (default: 2) |
pool_max | int | Maximum pool connections |
connect_timeout_ms | int | Connection timeout in milliseconds |
idle_timeout_ms | int | Idle connection timeout in milliseconds |
ssl_mode | string | "disable", "allow", "prefer", or "require" |
[database]url = "${DATABASE_URL}"[fraiseql]
Section titled “[fraiseql]”| Key | Type | Description |
|---|---|---|
schema_file | string | Input: JSON generated by Python decorators |
output_file | string | Output: compiled schema for the Rust runtime |
[security.audit_logging]
Section titled “[security.audit_logging]”| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable security audit logging |
log_level | string | "info" | "debug", "info", or "warn" |
include_sensitive_data | bool | false | Never true in production |
async_logging | bool | true | Non-blocking log writes |
buffer_size | int | 1000 | Events buffered before flush |
flush_interval_secs | int | 5 | Seconds between flushes |
[security.error_sanitization]
Section titled “[security.error_sanitization]”| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Sanitize error messages sent to clients |
generic_messages | bool | true | Always true in production |
internal_logging | bool | true | Log full details internally |
leak_sensitive_details | bool | false | Never true in production |
user_facing_format | string | "generic" | "generic", "simple", or "detailed" |
[security.rate_limiting]
Section titled “[security.rate_limiting]”| Key | Type | Description |
|---|---|---|
enabled | bool | Enable rate limiting |
auth_start_max_requests | int | Max /auth/start requests per IP per window |
auth_start_window_secs | int | Time window in seconds |
auth_callback_max_requests | int | Max /auth/callback requests per IP per window |
auth_callback_window_secs | int | Time window in seconds |
auth_refresh_max_requests | int | Max /auth/refresh requests per user per window |
auth_refresh_window_secs | int | Time window in seconds |
failed_login_max_requests | int | Max failed login attempts before lockout |
failed_login_window_secs | int | Lockout window in seconds |
[caching]
Section titled “[caching]”OIDC / Authentication
Section titled “OIDC / Authentication”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"[observers]
Section titled “[observers]”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]
Section titled “[federation]”[federation]enabled = trueentity_key = "id"Environment Variables
Section titled “Environment Variables”Secrets are never stored in fraiseql.toml. Reference them with ${VAR_NAME} syntax:
DATABASE_URL=postgresql://user_svc:password@user-db:5432/usersOIDC_CLIENT_ID=user-service-clientOIDC_ISSUER_URL=https://auth.example.comOIDC_REDIRECT_URI=https://users.example.com/auth/callbackSTATE_ENCRYPTION_KEY=<base64-32-bytes>DATABASE_URL=postgresql://order_svc:password@order-db:5432/ordersNATS_URL=nats://nats:4222OIDC_CLIENT_ID=order-service-clientOIDC_ISSUER_URL=https://auth.example.comOIDC_REDIRECT_URI=https://orders.example.com/auth/callbackSTATE_ENCRYPTION_KEY=<base64-32-bytes>Generate encryption keys:
export STATE_ENCRYPTION_KEY=$(openssl rand -base64 32)Gateway Configuration
Section titled “Gateway Configuration”FraiseQL offers two gateway options. For FraiseQL-only subgraphs, the built-in gateway is the simplest option. For mixed subgraphs, use Apollo Router.
Built-in Gateway (gateway.toml)
Section titled “Built-in Gateway (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 = 5recovery_timeout_secs = 30fraiseql gateway --config gateway.tomlSee Federation Gateway Guide for the full configuration reference.
Apollo Router Configuration
Section titled “Apollo Router Configuration”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.
supergraph: listen: 0.0.0.0:4000
# Forward the client's Authorization header to every subgraphheaders: all: request: - propagate: named: Authorization
subgraphs: users: routing_url: http://user-service:4001/graphql orders: routing_url: http://order-service:4002/graphqlComposing the Supergraph
Section titled “Composing the Supergraph”Use the Rover CLI to compose subgraph schemas into a supergraph:
federation_version: =2.4.0subgraphs: 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/graphqlrover supergraph compose --config supergraph.yaml > supergraph.graphqlrouter --config router.yaml --supergraph supergraph.graphqlDevelopment vs. Production TOML
Section titled “Development vs. Production TOML”Development
Section titled “Development”[security.audit_logging]log_level = "debug"include_sensitive_data = true # OK for local dev onlyasync_logging = false # Synchronous for easier debugging
[security.error_sanitization]user_facing_format = "detailed" # Show full errors locally
[security.rate_limiting]auth_start_max_requests = 10000failed_login_max_requests = 10000Production
Section titled “Production”[security.audit_logging]enabled = truelog_level = "info"include_sensitive_data = falseasync_logging = true
[security.error_sanitization]enabled = truegeneric_messages = trueleak_sensitive_details = false
[security.rate_limiting]enabled = trueauth_start_max_requests = 100failed_login_max_requests = 5failed_login_window_secs = 3600Complete Example: Two-Service Deployment
Section titled “Complete Example: Two-Service Deployment”File Layout
Section titled “File Layout”federation-example/├── user-service/│ ├── fraiseql.toml│ ├── schema.py│ └── Dockerfile├── order-service/│ ├── fraiseql.toml│ ├── schema.py│ └── Dockerfile├── router.yaml├── supergraph.yaml└── docker-compose.ymlDocker Compose
Section titled “Docker Compose”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-serviceBuild and Start
Section titled “Build and Start”# Step 1: Compile each servicecd 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 -drover supergraph compose --config supergraph.yaml > supergraph.graphql
# Step 3: Start everythingdocker-compose upMulti-Transport Subgraphs
Section titled “Multi-Transport Subgraphs”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.