CLI Reference
CLI Reference — all runtime flags
FraiseQL uses a single fraiseql.toml file for all project configuration. Secrets (database passwords, JWT keys) stay in environment variables — the TOML file references them by name.
| Section | Since | Stability |
|---|---|---|
[project] | — | Stable |
[database] | — | Stable |
[server] | — | Stable |
[server.cors] | — | Stable |
[server.tls] | — | Stable |
[fraiseql] | — | Stable |
[caching] | — | Stable |
[mcp] | — | Stable |
[query_defaults] | — | Stable |
[security] | — | Stable |
[security.enterprise] | — | Stable |
[security.error_sanitization] | — | Stable |
[security.constant_time] | — | Stable |
[security.rate_limiting] | — | Stable |
[security.state_encryption] | — | Stable |
[security.pkce] | — | Stable |
[security.api_keys] | — | Stable |
[security.token_revocation] | — | Stable |
[security.trusted_documents] | — | Stable |
[validation] | — | Stable |
[subscriptions] | — | Stable |
[analytics] | — | Stable |
[debug] | — | Stable |
[federation] | — | Stable |
[observers] | — | Stable |
[tracing] | 2.1.0 | Stable |
[server.limits] | 2.1.0 | Stable |
[rest] | v2.1 | Stable |
[grpc] | v2.1 | Stable |
[session_variables] | v2.1 | Stable |
[inject_defaults] | v2.1 | Stable |
[cascade] | v2.1 | Stable |
[dev] | v2.2 | Stable |
[gateway] | v2.1 | Stable |
my-project/├── fraiseql.toml├── schema.json└── db/Generated by fraiseql init. The only required fields are the database URL reference and schema path.
[project]name = "my-api"version = "0.1.0"database_target = "postgresql"
[database]url = "${DATABASE_URL}"
[fraiseql]schema_file = "schema.json"output_file = "schema.compiled.json"Run with:
DATABASE_URL="postgresql://localhost:5432/mydb" fraiseql runEvery TOML value can be overridden at runtime with a CLI flag or environment variable. CLI flags take precedence over environment variables, which take precedence over TOML.
# Override port at runtime without changing fraiseql.tomlfraiseql run --port 9000
# Environment variable override (FRAISEQL_ prefix, __ for nesting)FRAISEQL_SERVER__PORT=9000 fraiseql run| TOML key | Environment variable |
|---|---|
server.port | FRAISEQL_SERVER__PORT |
server.host | FRAISEQL_SERVER__HOST |
database.url | DATABASE_URL or FRAISEQL_DATABASE__URL |
database.pool_max | FRAISEQL_DATABASE__POOL_MAX |
Project metadata used during compilation.
[project]name = "my-api"version = "1.0.0"description = "My GraphQL API"database_target = "postgresql"| Field | Type | Default | Description |
|---|---|---|---|
name | string | "my-fraiseql-app" | Project identifier |
version | string | "1.0.0" | Semantic version |
description | string | — | Human-readable description |
database_target | string | — | Optional. postgresql, mysql, sqlite, sqlserver |
Database connection and connection pool settings. Never put credentials directly in this file — use ${ENV_VAR} references.
[database]url = "${DATABASE_URL}"pool_min = 2pool_max = 20connect_timeout_ms = 5000idle_timeout_ms = 600000ssl_mode = "prefer"| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | Connection URL or env var reference |
pool_min | integer | 2 | Minimum pool connections |
pool_max | integer | 20 | Maximum pool connections |
connect_timeout_ms | integer | 5000 | Connection acquisition timeout in ms |
idle_timeout_ms | integer | 600000 | Idle connection lifetime in ms |
ssl_mode | string | "prefer" | disable, allow, prefer, or require |
Env var reference syntax:
url = "${DATABASE_URL}" # Required env varurl = "${DATABASE_URL:-postgresql://localhost/mydb}" # With fallbackHTTP server binding and request handling.
[server]host = "0.0.0.0"port = 8080request_timeout_ms = 30000keep_alive_secs = 75admin_token = "${FRAISEQL_ADMIN_TOKEN}" # protects RBAC management + admin APIadmin_api_enabled = true # enable /api/v1/admin/* endpointsadmin_readonly_token = "${FRAISEQL_ADMIN_READONLY_TOKEN}" # optional: read-only admin access| Field | Type | Default | Description |
|---|---|---|---|
host | string | "0.0.0.0" | Bind address |
port | integer | 8080 | Listen port |
request_timeout_ms | integer | 30000 | Request timeout in ms |
keep_alive_secs | integer | 75 | TCP keep-alive in seconds |
admin_token | string | — | Bearer token protecting /api/rbac/* and /api/v1/admin/* write endpoints. If not set, both the RBAC management API and admin API are disabled entirely (endpoints return 404). Use FRAISEQL_ADMIN_TOKEN env var. Minimum 32 characters. |
admin_api_enabled | bool | false | Enable admin API endpoints (/api/v1/admin/*) for schema reload, cache management, and diagnostics. Requires admin_token to be set. |
admin_readonly_token | string | — | Separate bearer token for admin read-only endpoints (cache stats, config, explain). Falls back to admin_token if not set. Use FRAISEQL_ADMIN_READONLY_TOKEN env var. Minimum 32 characters. |
Cross-Origin Resource Sharing. Required when your frontend is served from a different origin.
[server.cors]origins = ["https://app.example.com", "http://localhost:3000"]credentials = true| Field | Type | Default | Description |
|---|---|---|---|
origins | array | [] | Allowed origins (empty = all origins) |
credentials | bool | false | Allow cookies and auth headers |
TLS termination. For most deployments, terminate TLS at the load balancer and run fraiseql on plain HTTP internally.
[server.tls]enabled = truecert_file = "/etc/ssl/certs/server.pem"key_file = "/etc/ssl/private/server.key"min_version = "1.2"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable TLS |
cert_file | string | — | Path to certificate file (PEM) |
key_file | string | — | Path to private key file (PEM) |
min_version | string | "1.2" | Minimum TLS version: "1.2" or "1.3" |
Compilation settings.
[fraiseql]schema_file = "schema.json"output_file = "schema.compiled.json"| Field | Type | Default | Description |
|---|---|---|---|
schema_file | string | "schema.json" | Path to the schema definition |
output_file | string | "schema.compiled.json" | Path for the compiled output |
[rest]enabled = truepath = "/rest/v1" # base path for all REST routesrequire_auth = false # require OIDC/JWT for all REST endpointsinclude = [] # whitelist operations by name (empty = all)exclude = [] # blacklist operations by namedelete_response = "no_content" # "no_content" (204) or "entity" (200 + body)max_page_size = 100 # maximum limit value for paginationdefault_page_size = 20 # default limit when not specifiedetag = true # enable ETag / If-None-Match conditional requestsmax_filter_bytes = 4096 # maximum size of ?filter= JSON parammax_embedding_depth = 3 # max nesting for ?select=posts(comments(...))openapi_enabled = false # serve OpenAPI 3.0.3 specopenapi_path = "/rest/v1/openapi.json" # path for OpenAPI spec endpointtitle = "My API" # OpenAPI info.titleapi_version = "1.0.0" # OpenAPI info.version| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable/disable REST transport. Default: true when the rest-transport Cargo feature is active. |
path | string | "/rest/v1" | Base URL path for all REST routes |
require_auth | string | false | Require OIDC/JWT authentication for all REST endpoints |
include | array | [] | Whitelist of operation names to expose (empty = all) |
exclude | array | [] | Blacklist of operation names to hide from REST |
delete_response | string | "no_content" | Default DELETE response: "no_content" (204) or "entity" (200 with body). Per-request override via Prefer: return=minimal or Prefer: return=representation header. |
max_page_size | integer | 100 | Maximum allowed limit value. Requests exceeding this are clamped. |
default_page_size | integer | 20 | Default limit when not specified in the request |
etag | bool | true | Enable ETag and If-None-Match conditional request support (xxHash64) |
max_filter_bytes | integer | 4096 | Maximum size in bytes for the ?filter= JSON query parameter |
max_embedding_depth | integer | 3 | Maximum nesting depth for ?select=posts(comments(...)) embedding |
openapi_enabled | bool | false | Serve OpenAPI 3.0.3 spec at openapi_path |
openapi_path | string | "/rest/v1/openapi.json" | Path for the OpenAPI spec endpoint |
title | string | "FraiseQL REST API" | OpenAPI info.title |
api_version | string | "1.0.0" | OpenAPI info.version |
max_bulk_affected | integer | 1000 | Maximum rows a bulk mutation can affect |
default_cache_ttl | integer | 60 | Default Cache-Control: max-age for GET responses (seconds) |
cdn_max_age | integer | — | s-maxage value for CDN proxies (seconds). Omitted from header if unset. |
idempotency_ttl_seconds | integer | 86400 | How long idempotency keys are retained |
[grpc]enabled = trueport = 50052reflection = truemax_message_size_bytes = 4194304descriptor_path = "proto/descriptor.binpb"stream_batch_size = 500# include_types = ["User", "Post"]# exclude_types = ["InternalAudit"]| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable gRPC transport |
port | integer | 50052 | gRPC listen port (separate from the HTTP server) |
reflection | bool | true | Enable gRPC server reflection for grpcurl / gRPC UI discovery |
max_message_size_bytes | integer | 4194304 | Maximum inbound message size in bytes (4 MiB) |
descriptor_path | string | "proto/descriptor.binpb" | Path to the compiled FileDescriptorSet binary |
stream_batch_size | integer | 500 | Rows per batch in server-streaming RPCs (list queries) |
include_types | string[] | [] | Whitelist of type names to expose as gRPC services (empty = all) |
exclude_types | string[] | [] | Blacklist of type names to hide from gRPC services |
gRPC runs on a separate port (default 50052) from the HTTP server.
Distributed tracing and OTLP export. OpenTelemetry tracing is compiled into the server by default — zero overhead when no endpoint is configured (no gRPC connection attempt).
[tracing]enabled = true # default: truelevel = "info" # log level filter: error | warn | info | debug | traceformat = "json" # log format: json | prettyservice_name = "fraiseql" # service name for distributed tracingotlp_endpoint = "http://otel-collector:4317" # OTLP exporter endpointotlp_export_timeout_secs = 10 # OTLP exporter timeout in seconds| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable tracing and structured logging |
level | string | "info" | Log level filter: error, warn, info, debug, trace |
format | string | "json" | Log output format: "json" (production) or "pretty" (development) |
service_name | string | "fraiseql" | Service name for distributed tracing spans |
otlp_endpoint | string | — | OTLP exporter endpoint (e.g. "http://otel-collector:4317"). When not set, falls back to the OTEL_EXPORTER_OTLP_ENDPOINT environment variable. If neither is set, no OTLP export occurs. |
otlp_export_timeout_secs | integer | 10 | How long the OTLP exporter waits for a collector response before timing out |
Per-request body size, timeout, and concurrency limits. These control the admission controller that protects the server from overload.
[server.limits]max_request_size = "10MB" # maximum request body sizerequest_timeout = "30s" # per-request processing timeoutmax_concurrent_requests = 1000 # simultaneous requests in flightmax_queue_depth = 5000 # requests waiting in the accept queue| Field | Type | Default | Description |
|---|---|---|---|
max_request_size | string | "10MB" | Maximum allowed request body size (human-readable, e.g. "10MB", "512KB") |
request_timeout | string | "30s" | Maximum time to process a single request (e.g. "30s", "5m") |
max_concurrent_requests | integer | 1000 | Maximum requests processed simultaneously. Excess requests are queued. |
max_queue_depth | integer | 5000 | Maximum requests waiting in the accept queue. Requests beyond this limit receive 503 Service Unavailable. |
Response caching for query results. Reduces database load for read-heavy workloads.
[caching]enabled = truebackend = "redis" # "memory" or "redis"redis_url = "${REDIS_URL}" # required when backend = "redis"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable response caching |
backend | string | "redis" | Cache backend: "memory" (single-instance) or "redis" (distributed) |
redis_url | string | — | Redis connection URL (required when backend = "redis") |
Per-query TTL is set via cache_ttl_seconds= in fraiseql.config() in the function body. See Caching.
Model Context Protocol server — exposes FraiseQL queries and mutations as MCP tools for AI agents (Claude Desktop, etc.).
[mcp]enabled = truetransport = "http" # "http" | "stdio" | "both"path = "/mcp" # HTTP endpoint pathrequire_auth = true # Require JWT/API key for MCP requests
# Restrict which operations are exposedinclude = [] # Whitelist by operation name (empty = all)exclude = [] # Blacklist by operation name| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable the MCP server |
transport | string | "http" | Transport: "http" (in-process endpoint), "stdio" (for CLI agents), or "both" |
path | string | "/mcp" | HTTP path for the MCP endpoint |
require_auth | bool | true | Require the same JWT/API key as the GraphQL endpoint |
include | array | [] | Whitelist of operation names to expose (empty = all) |
exclude | array | [] | Blacklist of operation names to hide from MCP clients |
See MCP Server for setup with Claude Desktop.
Project-wide defaults for which auto-parameters (where, order_by, limit, offset) are enabled on list queries. Per-query auto_params= overrides remain possible and are now partial — only specify the flags that differ from the project default.
Priority (lowest → highest):
[query_defaults] in fraiseql.tomlauto_params={"limit": True} in Python decorator[query_defaults]where = true # default: trueorder_by = true # default: truelimit = true # default: trueoffset = true # default: true| Field | Type | Default | Description |
|---|---|---|---|
where | bool | true | Enable where filter argument on list queries |
order_by | bool | true | Enable orderBy sort argument |
limit | bool | true | Enable limit argument |
offset | bool | true | Enable offset argument |
Example — Relay-first project (disable offset pagination globally):
[query_defaults]limit = falseoffset = false# Only this admin query re-enables limit/offset@fraiseql.querydef admin_logs() -> list[AdminLog]: return fraiseql.config(sql_source="v_admin_log", auto_params={"limit": True, "offset": True})Compile-time warnings:
limit = false on a non-relay list query → unbounded table scan warninglimit = true + order_by = false → non-deterministic pagination warningAuthorization policies enforced at runtime. JWT secret keys are never stored in TOML — only in environment variables. OIDC client configuration (issuer URL, client ID, redirect URI) is configured via environment variables (OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI). There is no [auth] TOML section — it does not exist and causes a parse error.
[security]default_policy = "authenticated" # or "public"
[[security.rules]]name = "owner_only"rule = "user.id == object.owner_id"description = "User can only access their own data"cacheable = truecache_ttl_seconds = 300
[[security.policies]]name = "admin_only"type = "rbac" # rbac | abac | custom | hybridroles = ["admin"]strategy = "any" # any | all | exactlycache_ttl_seconds = 600
[[security.field_auth]]type_name = "User"field_name = "email"policy = "admin_only"| Field | Type | Default | Description |
|---|---|---|---|
default_policy | string | "authenticated" | Default access: "authenticated" or "public" |
Enterprise feature flags for audit logging and legacy rate limiting.
[security.enterprise]rate_limiting_enabled = trueauth_endpoint_max_requests = 100auth_endpoint_window_seconds = 60audit_logging_enabled = trueaudit_log_backend = "postgresql"| Field | Type | Default | Description |
|---|---|---|---|
rate_limiting_enabled | bool | true | Enable built-in rate limiting |
auth_endpoint_max_requests | integer | 100 | Max auth endpoint requests per window |
auth_endpoint_window_seconds | integer | 60 | Auth rate limit window in seconds |
audit_logging_enabled | bool | true | Enable audit logging |
audit_log_backend | string | "postgresql" | Audit log storage backend (e.g. "postgresql") |
audit_retention_days | integer | 365 | How many days to retain audit log entries |
Controls what error detail is returned to clients. Disabled by default — enable in production to prevent SQL errors and stack traces from reaching clients.
[security.error_sanitization]enabled = false # opt-inhide_implementation_details = true # maximally safe when enabledsanitize_database_errors = truecustom_error_message = "An internal error occurred"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable error sanitization (opt-in) |
hide_implementation_details | bool | true | Strip stack traces and implementation paths |
sanitize_database_errors | bool | true | Replace DB error messages with generic text |
custom_error_message | string | "An internal error occurred" | Message sent to clients when sanitizing |
ValidationError, Forbidden, and NotFound codes pass through unchanged — clients need these to act on errors. Only InternalServerError and DatabaseError codes are sanitized.
Enables constant-time comparison for token validation to prevent timing attacks. All fields default to true — this section only needs to appear in fraiseql.toml if you need to selectively disable individual token types.
[security.constant_time]enabled = trueapply_to_jwt = trueapply_to_session_tokens = trueapply_to_csrf_tokens = trueapply_to_refresh_tokens = true| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Master switch for constant-time comparisons |
apply_to_jwt | bool | true | Use constant-time comparison for JWT signature validation |
apply_to_session_tokens | bool | true | Use constant-time comparison for session tokens |
apply_to_csrf_tokens | bool | true | Use constant-time comparison for CSRF tokens |
apply_to_refresh_tokens | bool | true | Use constant-time comparison for refresh tokens |
Token bucket rate limiting applied globally, plus per-endpoint limits for auth routes. In-memory per instance (no cross-instance coordination).
[security.rate_limiting]enabled = false # opt-inrequests_per_second = 100requests_per_second_per_user = 1000 # default: 10× requests_per_secondburst_size = 200trust_proxy_headers = false # set true only when behind a trusted reverse proxyauth_start_max_requests = 5auth_start_window_secs = 60auth_callback_max_requests = 10auth_callback_window_secs = 60auth_refresh_max_requests = 20auth_refresh_window_secs = 300auth_logout_max_requests = 30auth_logout_window_secs = 60failed_login_max_attempts = 10failed_login_lockout_secs = 900# redis_url = "${REDIS_URL}" # requires redis-rate-limiting Cargo feature| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable rate limiting (opt-in) |
requests_per_second | integer | 100 | Global token bucket refill rate |
requests_per_second_per_user | integer | requests_per_second × 10 | Per-authenticated-user limit |
burst_size | integer | 200 | Maximum burst above steady-state rate |
trust_proxy_headers | bool | false | Read X-Real-IP / X-Forwarded-For for client IP. Enable only when behind a trusted reverse proxy — spoofable otherwise |
redis_url | string | — | Redis connection URL for distributed rate limiting (requires redis-rate-limiting feature) |
auth_start_max_requests | integer | 5 | Max /auth/start requests per window |
auth_start_window_secs | integer | 60 | Window for /auth/start limit |
auth_callback_max_requests | integer | 10 | Max /auth/callback requests per window |
auth_callback_window_secs | integer | 60 | Window for /auth/callback limit |
auth_refresh_max_requests | integer | 20 | Max /auth/refresh requests per window |
auth_refresh_window_secs | integer | 300 | Window for /auth/refresh limit |
auth_logout_max_requests | integer | 30 | Max /auth/logout requests per window |
auth_logout_window_secs | integer | 60 | Window for /auth/logout limit |
failed_login_max_attempts | integer | 10 | Failed auth attempts before lockout |
failed_login_lockout_secs | integer | 900 | Lockout duration in seconds |
AEAD encryption for OAuth state blobs stored between /auth/start and /auth/callback. Requires a 32-byte key as a 64-character hex string.
[security.state_encryption]enabled = false # opt-inalgorithm = "chacha20-poly1305" # or "aes-256-gcm"key_source = "env"key_env = "STATE_ENCRYPTION_KEY"Generate a key: openssl rand -hex 32
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable state encryption (opt-in) |
algorithm | string | "chacha20-poly1305" | AEAD cipher: "chacha20-poly1305" or "aes-256-gcm" |
key_source | string | "env" | Key source — only "env" supported |
key_env | string | "STATE_ENCRYPTION_KEY" | Environment variable with the 64-char hex key |
PKCE (Proof Key for Code Exchange) for the OAuth /auth/start → /auth/callback flow.
[security.pkce]enabled = false # opt-in; requires state_encryption.enabled = truecode_challenge_method = "S256" # or "plain" (warns at startup in non-dev environments)state_ttl_secs = 600# redis_url = "${REDIS_URL}" # requires redis-pkce Cargo feature| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable PKCE (opt-in) |
code_challenge_method | string | "S256" | Challenge method: "S256" (recommended) or "plain" |
state_ttl_secs | integer | 600 | OAuth state token lifetime in seconds |
redis_url | string | — | Redis connection URL for distributed PKCE state (requires redis-pkce feature) |
API key authentication for service-to-service and CI/CD access. Keys are stored hashed — the plaintext is returned once at creation and never stored.
[security.api_keys]enabled = trueheader = "X-API-Key" # HTTP header namehash_algorithm = "sha256" # "sha256" (fast) or "argon2" (production)storage = "postgres" # "postgres" or "env" (static keys, CI only)
# Static keys — testing and CI only. Never in production.[[security.api_keys.static]]key_hash = "sha256:abc123..." # echo -n "secret" | sha256sumscopes = ["read:*"]name = "ci-readonly"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable API key authentication |
header | string | "X-API-Key" | HTTP header to read the key from |
hash_algorithm | string | "sha256" | Hashing algorithm: "sha256" or "argon2" |
storage | string | "postgres" | Key storage backend: "postgres" (production) or "env" (static, CI only) |
For production, store hashed keys in the fraiseql_api_keys table. See Authentication for the table schema and mutation patterns.
Revoke JWTs before their exp claim expires — for logout, key rotation, or security incidents.
[security.token_revocation]enabled = truebackend = "redis" # "redis" or "postgres"require_jti = true # reject JWTs without a jti claimfail_open = false # if store unreachable, deny (not allow) the request| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable token revocation |
backend | string | — | Storage backend: "redis" (recommended) or "postgres" |
require_jti | bool | false | Reject tokens that have no jti claim |
fail_open | bool | false | When false (default), deny requests if the revocation store is unreachable. Set true to allow through on store failure |
When enabled, two endpoints become available:
POST /auth/revoke — revoke the caller’s own token (self-logout)POST /auth/revoke-all — revoke all tokens for a user (requires admin:revoke scope)Revoked JTIs are stored until the token’s exp expires — no manual cleanup needed.
Query allowlisting — only permit GraphQL operations present in a pre-approved manifest. Useful for hardening production APIs against arbitrary query execution.
[security.trusted_documents]enabled = truemode = "strict" # "strict" | "permissive"manifest_path = "./trusted-documents.json"reload_interval_secs = 300 # 0 = no hot-reload| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable trusted document enforcement |
mode | string | "permissive" | "strict" rejects unknown operations; "permissive" logs them but allows through |
manifest_path | string | — | Path to local JSON manifest (hash → query) |
manifest_url | string | — | URL to fetch the manifest from at startup |
reload_interval_secs | integer | 0 | How often to poll for manifest changes (0 = no reload) |
Query depth and complexity limits enforced at parse time, before execution. Queries exceeding either limit receive a validation error — no database round-trip occurs.
[validation]max_query_depth = 10max_query_complexity = 100| Field | Type | Default | Description |
|---|---|---|---|
max_query_depth | integer | 10 | Maximum allowed field nesting depth |
max_query_complexity | integer | 100 | Maximum allowed query complexity score |
Complexity is calculated by summing field weights (lists count more than scalars). Adjust thresholds based on your schema — a schema with many list fields may need a higher limit for legitimate queries.
WebSocket subscription settings. FraiseQL supports both graphql-transport-ws (modern) and graphql-ws (Apollo legacy) — protocol is negotiated from the Sec-WebSocket-Protocol header automatically.
[subscriptions]max_subscriptions_per_connection = 10
[subscriptions.hooks]on_connect = "https://auth.example.com/ws/connect"on_subscribe = "https://auth.example.com/ws/subscribe"on_disconnect = "https://auth.example.com/ws/disconnect"timeout_ms = 500| Field | Type | Default | Description |
|---|---|---|---|
max_subscriptions_per_connection | integer | unlimited | Maximum concurrent subscriptions per WebSocket connection |
[subscriptions.hooks] — webhook URLs invoked during connection lifecycle:
| Field | Type | Default | Description |
|---|---|---|---|
on_connect | string | — | URL to POST on connection_init. Fail-closed: connection rejected if hook returns non-2xx |
on_subscribe | string | — | URL to POST before a subscription is registered. Fail-closed |
on_disconnect | string | — | URL to POST on WebSocket close. Fire-and-forget (errors ignored) |
timeout_ms | integer | 500 | Timeout for fail-closed hooks (on_connect, on_subscribe) |
Development and diagnostics features. All debug capabilities are disabled by default and gated behind a master enabled switch. Never enable in production.
[debug]enabled = true # master switch — required for all other flagsdatabase_explain = true # include EXPLAIN output in /api/v1/query/explain responsesexpose_sql = true # include generated SQL in explain responses (default when enabled)| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Master switch. All debug features require this to be true |
database_explain | bool | false | Run EXPLAIN against the database and include the query plan in /api/v1/query/explain responses |
expose_sql | bool | true | Include generated SQL in explain responses |
Explain endpoint:
POST /api/v1/query/explainContent-Type: application/json
{"query": "{ users { id name } }"}Response includes the generated SQL and (when database_explain = true) the Postgres query plan. Use this to diagnose slow queries without enabling general query logging.
Analytics query cache and Arrow Flight columnar query definitions. Controls whether the analytics subsystem is active.
[analytics]enabled = false| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Controls the analytics query cache and Arrow Flight columnar query definitions. |
Apollo Federation v2 support and multi-database federation settings.
[federation]enabled = trueapollo_version = 2
[[federation.entities]]name = "User"key_fields = ["id"]Per-entity circuit breaker for Apollo Federation subgraph calls. Automatically opens after consecutive errors, returning 503 Service Unavailable with a Retry-After header instead of cascading to a gateway timeout.
[federation.circuit_breaker]enabled = truefailure_threshold = 5 # open after N consecutive errorsrecovery_timeout_secs = 30 # half-open probe after this many secondssuccess_threshold = 2 # successes in HalfOpen needed to close
# Per-entity override[[federation.circuit_breaker.per_database]]database = "Order"failure_threshold = 3recovery_timeout_secs = 60| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable the circuit breaker |
failure_threshold | integer | 5 | Consecutive errors before opening |
recovery_timeout_secs | integer | 30 | Seconds before attempting a recovery probe |
success_threshold | integer | 2 | Successes in HalfOpen state before closing |
[[federation.circuit_breaker.per_database]] accepts an array of per-entity overrides. Each entry requires a database key matching the federation entity name. All three threshold fields are optional (inherit from the global config if omitted).
Prometheus gauge: fraiseql_federation_circuit_breaker_state{entity="..."} (0 = Closed, 1 = Open, 2 = HalfOpen).
Event observer system. Supports in-memory, Redis, NATS, and PostgreSQL backends.
When enabled, the observer runtime runs inside the server process and adds:
LISTEN/NOTIFY)max_concurrency workers)channel_capacity × average event size + up to max_dlq_size × average DLQ entry[observers]enabled = truebackend = "redis" # "redis" | "nats" | "postgresql" | "in-memory"max_concurrency = 50 # thread pool for concurrent action executionchannel_capacity = 1000 # in-process event buffer sizemax_dlq_size = 10000 # dead letter queue cap; set this in productioncheckpoint_strategy = "at_least_once" # "at_least_once" or "effectively_once"idempotency_table = "observer_idempotency_keys" # required when checkpoint_strategy = "effectively_once"nats_url = "${FRAISEQL_NATS_URL}" # required when backend = "nats"# redis_url = "${REDIS_URL}" # required when backend = "redis"
[[observers.handlers]]name = "notify-new-user"event = "user.created"action = "webhook"webhook_url = "https://api.example.com/notify"synchronous = false # true = block mutation response until complete| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable the observer system |
backend | string | "redis" | Event backend: redis, nats, postgresql, in-memory |
max_concurrency | integer | 50 | Maximum concurrent action executions (thread pool size). Size to match your expected peak event rate and action latency. |
channel_capacity | integer | 1000 | In-process event buffer. Events exceeding capacity trigger backpressure. |
max_dlq_size | integer | unbounded | Dead letter queue maximum entries. Always set in production to prevent unbounded memory growth on persistent action failures. Use null to keep unbounded (not recommended). |
checkpoint_strategy | string | "at_least_once" | "at_least_once" (fast, may duplicate) or "effectively_once" (dedup via PostgreSQL idempotency table) |
idempotency_table | string | "observer_idempotency_keys" | PostgreSQL table for deduplication (required when checkpoint_strategy = "effectively_once") |
nats_url | string | — | NATS server URL (required when backend = "nats"). Override via FRAISEQL_NATS_URL. |
redis_url | string | — | Redis connection URL (required when backend = "redis"). |
[[observers.handlers]] — each entry maps an event to an action:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Handler identifier |
event | string | required | Event name to match (e.g., user.created) |
action | string | required | Action type: "webhook" |
webhook_url | string | — | URL to POST to (required when action = "webhook") |
synchronous | bool | false | Block mutation response until this handler completes |
| Workload | max_concurrency | channel_capacity | max_dlq_size |
|---|---|---|---|
| Low volume (< 100 events/s) | 10 | 500 | 1,000 |
| Medium (100–1,000 events/s) | 50 (default) | 2,000 | 10,000 |
| High (> 1,000 events/s) | 100–200 | 5,000 | 50,000 |
See Observer Operations Runbook for DLQ recovery procedures.
Inject per-request context into PostgreSQL via SET LOCAL before each query/mutation. SQL views and functions can read these values with current_setting('app.tenant_id').
[session_variables]inject_started_at = true # auto-inject fraiseql.started_at timestamp (default: true)
[[session_variables.variables]]pg_name = "app.tenant_id"source = "jwt"claim = "tenant_id"
[[session_variables.variables]]pg_name = "app.locale"source = "header"name = "Accept-Language"
[[session_variables.variables]]pg_name = "app.user_id"source = "jwt"claim = "sub"| Field | Type | Default | Description |
|---|---|---|---|
inject_started_at | boolean | true | Auto-inject fraiseql.started_at timestamp |
[[session_variables.variables]] — each entry maps a PostgreSQL GUC variable to a request context source:
| Field | Type | Required | Description |
|---|---|---|---|
pg_name | string | yes | PostgreSQL variable name (e.g., app.tenant_id) |
source | string | yes | "jwt" or "header" |
claim | string | when source = "jwt" | JWT claim name to extract |
name | string | when source = "header" | HTTP header name to extract |
Use app.* namespaced variables (PostgreSQL custom GUC prefix) to avoid conflicts with built-in settings. Access in SQL:
CREATE VIEW v_tenant_posts ASSELECT id, jsonb_build_object(...) AS dataFROM tb_postWHERE tenant_id = current_setting('app.tenant_id')::uuid;See Multi-Tenancy for the full RLS pattern.
Apply inject parameters globally to all queries and/or mutations, instead of repeating inject={"tenant_id": "jwt:tenant_id"} on every decorator.
[inject_defaults]tenant_id = "jwt:tenant_id" # applied to ALL queries and mutations
[inject_defaults.queries]# Query-specific overrides (merged with top-level defaults)
[inject_defaults.mutations]user_id = "jwt:sub" # applied to mutations only| Field | Type | Description |
|---|---|---|
| Top-level keys | string | Applied to both queries and mutations |
[inject_defaults.queries] | table | Applied to queries only (merged with top-level) |
[inject_defaults.mutations] | table | Applied to mutations only (merged with top-level) |
Per-decorator inject= overrides take precedence over defaults. This is particularly useful with tenant_scoped=True on @fraiseql.type, which generates the inject configuration automatically.
Controls the GraphQL Cascade protocol — automatic cache consistency through mutation responses that include all affected entities.
When enabled, mutations whose SQL functions return cascade data in mutation_response.cascade will expose it in the GraphQL response. The server also feeds cascade entities into the cache invalidation pipeline, evicting stale entries automatically.
[cascade]enabled = true # expose cascade data in mutation responses (default: false)| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Global default: include cascade field in mutation responses when the SQL function returns cascade data. Per-mutation overrides via cascade=True on the @fraiseql.mutation decorator take precedence. |
When enabled = true, the compiler auto-generates Cascade GraphQL types (Cascade, CascadeEntity, CascadeInvalidation, CascadeMetadata) and adds a cascade: Cascade field to every mutation success type. When enabled = false (default), you can still opt in per-mutation using the decorator parameter.
Development-only settings. Ignored when FRAISEQL_ENV=production.
[dev]enabled = truedefault_claims = { tenant_id = "dev-tenant", sub = "dev-user" }| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable dev mode (bypass JWT for inject_params) |
default_claims | table | {} | Default JWT claims when no token is present |
See Dev Mode Guide for usage.
gateway.toml)The federation gateway uses a separate config file from fraiseql.toml. See Federation Gateway Guide for usage.
[gateway]port = 4000bind = "0.0.0.0"
[[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 = 30
[gateway.cache]sdl_ttl_secs = 300query_plan_cache_size = 1000[gateway]
| Field | Type | Default | Description |
|---|---|---|---|
port | integer | 4000 | Gateway listen port |
bind | string | "0.0.0.0" | Bind address |
[[gateway.subgraphs]]
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Subgraph identifier |
url | string | required | Subgraph GraphQL endpoint URL |
[gateway.circuit_breaker]
| Field | Type | Default | Description |
|---|---|---|---|
failure_threshold | integer | 5 | Failures before circuit opens |
recovery_timeout_secs | integer | 30 | Seconds before half-open retry |
[gateway.cache]
| Field | Type | Default | Description |
|---|---|---|---|
sdl_ttl_secs | integer | 300 | Cache subgraph SDL for this duration |
query_plan_cache_size | integer | 1000 | Max cached query plans |
[project]name = "blog-api"version = "1.0.0"description = "Blog GraphQL API"database_target = "postgresql"
[database]url = "${DATABASE_URL}"pool_min = 5pool_max = 50connect_timeout_ms = 5000ssl_mode = "prefer"
[server]host = "0.0.0.0"port = 8080request_timeout_ms = 30000
[server.limits]max_concurrent_requests = 1000max_queue_depth = 5000
[server.cors]origins = ["https://blog.example.com", "http://localhost:3000"]credentials = true
[fraiseql]schema_file = "schema.json"output_file = "schema.compiled.json"
[security]default_policy = "authenticated"
[[security.policies]]name = "admin_only"type = "rbac"roles = ["admin"]strategy = "any"
[security.enterprise]audit_logging_enabled = trueaudit_log_backend = "postgresql"
[security.error_sanitization]enabled = true
[security.rate_limiting]enabled = truerequests_per_second = 100burst_size = 200auth_start_max_requests = 5failed_login_max_attempts = 5failed_login_lockout_secs = 900
[security.api_keys]enabled = truehash_algorithm = "argon2"storage = "postgres"
[security.token_revocation]enabled = truebackend = "redis"require_jti = truefail_open = false
[security.trusted_documents]enabled = truemode = "strict"manifest_path = "./trusted-documents.json"
[tracing]service_name = "blog-api"otlp_endpoint = "http://otel-collector:4317"
[validation]max_query_depth = 10max_query_complexity = 100
[subscriptions]max_subscriptions_per_connection = 10
[cascade]enabled = trueRun in production:
DATABASE_URL="postgresql://..." fraiseql runCLI Reference
CLI Reference — all runtime flags
Security
Security features — detailed security guide
Deployment
Deployment — production configuration