fraiseql_server/server_config/mod.rs
1//! Server configuration (`*Config` types).
2//!
3//! These are developer-facing configuration types loaded from `fraiseql.toml`,
4//! environment variables, or CLI flags. They are mutable between deployments.
5//!
6//! For the distinction between `*Config` (developer-facing, mutable) and
7//! `*Settings` (compiled into `schema.compiled.json`, immutable at runtime),
8//! see `docs/architecture/config-vs-settings.md`.
9
10pub(crate) mod defaults;
11pub mod hs256;
12mod methods;
13pub mod observers;
14pub mod tls;
15
16#[cfg(test)]
17mod tests;
18
19use std::{net::SocketAddr, path::PathBuf};
20
21use defaults::{
22 default_bind_addr, default_database_url, default_graphql_path, default_health_path,
23 default_introspection_path, default_max_header_bytes, default_max_header_count,
24 default_max_request_body_bytes, default_metrics_json_path, default_metrics_path,
25 default_playground_path, default_pool_max_size, default_pool_min_size, default_pool_timeout,
26 default_readiness_path, default_schema_path, default_shutdown_timeout_secs,
27 default_subscription_path,
28};
29use fraiseql_core::security::OidcConfig;
30pub use hs256::Hs256Config;
31pub use observers::AdmissionConfig;
32#[cfg(feature = "observers")]
33pub use observers::{ObserverConfig, ObserverPoolConfig};
34use serde::{Deserialize, Serialize};
35pub use tls::{DatabaseTlsConfig, PlaygroundTool, TlsServerConfig};
36
37use crate::middleware::RateLimitConfig;
38
39/// Server configuration.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ServerConfig {
42 /// Path to compiled schema JSON file.
43 #[serde(default = "defaults::default_schema_path")]
44 pub schema_path: PathBuf,
45
46 /// Database connection URL (PostgreSQL, MySQL, SQLite, SQL Server).
47 #[serde(default = "defaults::default_database_url")]
48 pub database_url: String,
49
50 /// Server bind address.
51 #[serde(default = "defaults::default_bind_addr")]
52 pub bind_addr: SocketAddr,
53
54 /// Arrow Flight gRPC bind address (requires `arrow` feature).
55 ///
56 /// Defaults to `0.0.0.0:50051`. Override with `FRAISEQL_FLIGHT_BIND_ADDR`
57 /// environment variable or this field in the config file.
58 #[cfg(feature = "arrow")]
59 #[serde(default = "defaults::default_flight_bind_addr")]
60 pub flight_bind_addr: SocketAddr,
61
62 /// Enable CORS.
63 #[serde(default = "defaults::default_true")]
64 pub cors_enabled: bool,
65
66 /// CORS allowed origins (if empty, allows all).
67 #[serde(default)]
68 pub cors_origins: Vec<String>,
69
70 /// Enable framework-level response compression.
71 ///
72 /// Defaults to `false`. In production FraiseQL is typically deployed
73 /// behind a reverse proxy (Nginx, Caddy, cloud load balancer) that
74 /// handles compression more efficiently (brotli, shared across upstreams,
75 /// cacheable). Enable this only for single-binary / no-proxy deployments.
76 #[serde(default = "defaults::default_false")]
77 pub compression_enabled: bool,
78
79 /// Enable request tracing.
80 #[serde(default = "defaults::default_true")]
81 pub tracing_enabled: bool,
82
83 /// OTLP exporter endpoint for distributed tracing.
84 ///
85 /// When set (e.g. `"http://otel-collector:4317"`), the server initializes an
86 /// `OpenTelemetry` OTLP exporter. When `None`, the `OTEL_EXPORTER_OTLP_ENDPOINT`
87 /// environment variable is checked as a fallback. If neither is set, no OTLP
88 /// export occurs (zero overhead).
89 #[serde(default)]
90 pub otlp_endpoint: Option<String>,
91
92 /// OTLP exporter timeout in seconds (default: 10).
93 #[serde(default = "defaults::default_otlp_timeout_secs")]
94 pub otlp_export_timeout_secs: u64,
95
96 /// Service name for distributed tracing (default: `"fraiseql"`).
97 #[serde(default = "defaults::default_service_name")]
98 pub tracing_service_name: String,
99
100 /// Enable APQ (Automatic Persisted Queries).
101 #[serde(default = "defaults::default_true")]
102 pub apq_enabled: bool,
103
104 /// Enable query caching.
105 #[serde(default = "defaults::default_true")]
106 pub cache_enabled: bool,
107
108 /// GraphQL endpoint path.
109 #[serde(default = "defaults::default_graphql_path")]
110 pub graphql_path: String,
111
112 /// Health check endpoint path (liveness probe).
113 ///
114 /// Returns 200 as long as the process is alive, 503 if the database is down.
115 #[serde(default = "defaults::default_health_path")]
116 pub health_path: String,
117
118 /// Readiness probe endpoint path.
119 ///
120 /// Returns 200 when the server is ready to serve traffic (database reachable),
121 /// 503 otherwise. Kubernetes `readinessProbe` should point here.
122 #[serde(default = "defaults::default_readiness_path")]
123 pub readiness_path: String,
124
125 /// Introspection endpoint path.
126 #[serde(default = "defaults::default_introspection_path")]
127 pub introspection_path: String,
128
129 /// Metrics endpoint path (Prometheus format).
130 #[serde(default = "defaults::default_metrics_path")]
131 pub metrics_path: String,
132
133 /// Metrics JSON endpoint path.
134 #[serde(default = "defaults::default_metrics_json_path")]
135 pub metrics_json_path: String,
136
137 /// Playground (GraphQL IDE) endpoint path.
138 #[serde(default = "defaults::default_playground_path")]
139 pub playground_path: String,
140
141 /// Enable GraphQL playground/IDE (default: false for production safety).
142 ///
143 /// When enabled, serves a GraphQL IDE (`GraphiQL` or Apollo Sandbox)
144 /// at the configured `playground_path`.
145 ///
146 /// **Security**: Disabled by default for production safety. Set to true for development
147 /// environments only. The playground exposes schema information and can be a
148 /// reconnaissance vector for attackers.
149 #[serde(default)]
150 pub playground_enabled: bool,
151
152 /// Which GraphQL IDE to use.
153 ///
154 /// - `graphiql`: The classic GraphQL IDE (default)
155 /// - `apollo-sandbox`: Apollo's embeddable sandbox
156 #[serde(default)]
157 pub playground_tool: PlaygroundTool,
158
159 /// `WebSocket` endpoint path for GraphQL subscriptions.
160 #[serde(default = "defaults::default_subscription_path")]
161 pub subscription_path: String,
162
163 /// Enable GraphQL subscriptions over `WebSocket`.
164 ///
165 /// When enabled, provides graphql-ws (graphql-transport-ws) protocol
166 /// support for real-time subscription events.
167 #[serde(default = "defaults::default_true")]
168 pub subscriptions_enabled: bool,
169
170 /// Enable metrics endpoints.
171 ///
172 /// **Security**: Disabled by default for production safety.
173 /// When enabled, requires `metrics_token` to be set for authentication.
174 #[serde(default)]
175 pub metrics_enabled: bool,
176
177 /// Bearer token for metrics endpoint authentication.
178 ///
179 /// Required when `metrics_enabled` is true. Requests must include:
180 /// `Authorization: Bearer <token>`
181 ///
182 /// **Security**: Use a strong, random token (e.g., 32+ characters).
183 #[serde(default)]
184 pub metrics_token: Option<String>,
185
186 /// Enable admin API endpoints (default: false for production safety).
187 ///
188 /// **Security**: Disabled by default. When enabled, requires `admin_token` to be set.
189 /// Admin endpoints allow schema reloading, cache management, and config inspection.
190 #[serde(default)]
191 pub admin_api_enabled: bool,
192
193 /// Bearer token for admin API authentication.
194 ///
195 /// Required when `admin_api_enabled` is true. Requests must include:
196 /// `Authorization: Bearer <token>`
197 ///
198 /// **Security**: Use a strong, random token (minimum 32 characters).
199 /// This token grants access to **destructive** admin operations:
200 /// `reload-schema`, `cache/clear`.
201 ///
202 /// If `admin_readonly_token` is set, this token is restricted to write
203 /// operations only. If `admin_readonly_token` is not set, this token
204 /// also grants access to read-only endpoints (backwards-compatible).
205 #[serde(default)]
206 pub admin_token: Option<String>,
207
208 /// Optional separate bearer token for read-only admin operations.
209 ///
210 /// When set, restricts `admin_token` to destructive operations only
211 /// (`reload-schema`, `cache/clear`) and uses this token for read-only
212 /// endpoints (`config`, `cache/stats`, `explain`, `grafana-dashboard`).
213 ///
214 /// Operators and monitoring tools can use this token without gaining
215 /// the ability to modify server state or reload the schema.
216 ///
217 /// **Security**: Must be different from `admin_token` and at least 32
218 /// characters. Requires `admin_api_enabled = true` and `admin_token` set.
219 #[serde(default)]
220 pub admin_readonly_token: Option<String>,
221
222 /// Enable introspection endpoint (default: false for production safety).
223 ///
224 /// **Security**: Disabled by default. When enabled, the introspection endpoint
225 /// exposes the complete GraphQL schema structure. Combined with `introspection_require_auth`,
226 /// you can optionally protect it with OIDC authentication.
227 #[serde(default)]
228 pub introspection_enabled: bool,
229
230 /// Require authentication for introspection endpoint (default: true).
231 ///
232 /// When true and OIDC is configured, introspection requires same auth as GraphQL endpoint.
233 /// When false, introspection is publicly accessible (use only in development).
234 #[serde(default = "defaults::default_true")]
235 pub introspection_require_auth: bool,
236
237 /// Require authentication for design audit API endpoints (default: true).
238 ///
239 /// Design audit endpoints expose system architecture and optimization opportunities.
240 /// When true and OIDC is configured, design endpoints require same auth as GraphQL endpoint.
241 /// When false, design endpoints are publicly accessible (use only in development).
242 #[serde(default = "defaults::default_true")]
243 pub design_api_require_auth: bool,
244
245 /// Database connection pool minimum size.
246 #[serde(default = "defaults::default_pool_min_size")]
247 pub pool_min_size: usize,
248
249 /// Database connection pool maximum size.
250 #[serde(default = "defaults::default_pool_max_size")]
251 pub pool_max_size: usize,
252
253 /// Database connection pool timeout in seconds.
254 #[serde(default = "defaults::default_pool_timeout")]
255 pub pool_timeout_secs: u64,
256
257 /// OIDC authentication configuration (optional).
258 ///
259 /// When set, enables JWT authentication using OIDC discovery.
260 /// Supports Auth0, Keycloak, Okta, Cognito, Azure AD, and any
261 /// OIDC-compliant provider.
262 ///
263 /// # Example (TOML)
264 ///
265 /// ```toml
266 /// [auth]
267 /// issuer = "https://your-tenant.auth0.com/"
268 /// audience = "your-api-identifier"
269 /// ```
270 #[serde(default)]
271 pub auth: Option<OidcConfig>,
272
273 /// HS256 symmetric-key authentication (optional).
274 ///
275 /// Alternative to `auth` (OIDC) for integration testing and internal
276 /// service-to-service scenarios. Mutually exclusive with `auth`.
277 ///
278 /// Validation is fully local — no discovery endpoint, no JWKS fetch.
279 /// Not recommended for public-facing production.
280 ///
281 /// # Example (TOML)
282 ///
283 /// ```toml
284 /// [auth_hs256]
285 /// secret_env = "FRAISEQL_HS256_SECRET"
286 /// issuer = "my-test-suite"
287 /// audience = "my-api"
288 /// ```
289 #[serde(default)]
290 pub auth_hs256: Option<Hs256Config>,
291
292 /// TLS/SSL configuration for HTTPS and encrypted connections.
293 ///
294 /// When set, enables TLS enforcement for HTTP/gRPC endpoints and
295 /// optionally requires mutual TLS (mTLS) for client certificates.
296 ///
297 /// # Example (TOML)
298 ///
299 /// ```toml
300 /// [tls]
301 /// enabled = true
302 /// cert_path = "/etc/fraiseql/cert.pem"
303 /// key_path = "/etc/fraiseql/key.pem"
304 /// require_client_cert = false
305 /// min_version = "1.2" # "1.2" or "1.3"
306 /// ```
307 #[serde(default)]
308 pub tls: Option<TlsServerConfig>,
309
310 /// Database TLS configuration.
311 ///
312 /// Enables TLS for database connections and configures
313 /// per-database TLS settings (PostgreSQL, Redis, `ClickHouse`, etc.).
314 ///
315 /// # Example (TOML)
316 ///
317 /// ```toml
318 /// [database_tls]
319 /// postgres_ssl_mode = "require" # disable, allow, prefer, require, verify-ca, verify-full
320 /// redis_ssl = true # Use rediss:// protocol
321 /// clickhouse_https = true # Use HTTPS
322 /// elasticsearch_https = true # Use HTTPS
323 /// verify_certificates = true # Verify server certificates
324 /// ```
325 #[serde(default)]
326 pub database_tls: Option<DatabaseTlsConfig>,
327
328 /// Require `Content-Type: application/json` on POST requests (default: true).
329 ///
330 /// CSRF protection: rejects POST requests with non-JSON Content-Type
331 /// (e.g. `text/plain`, `application/x-www-form-urlencoded`) with 415.
332 #[serde(default = "defaults::default_true")]
333 pub require_json_content_type: bool,
334
335 /// Maximum request body size in bytes (default: 1 MB).
336 ///
337 /// Requests exceeding this limit receive 413 Payload Too Large.
338 /// Set to 0 to use axum's default (no limit).
339 #[serde(default = "defaults::default_max_request_body_bytes")]
340 pub max_request_body_bytes: usize,
341
342 /// Maximum number of HTTP headers per request (default: 100).
343 ///
344 /// Requests with more headers than this limit receive 431 Request Header Fields Too Large.
345 /// Prevents header-flooding `DoS` attacks that exhaust memory.
346 #[serde(default = "defaults::default_max_header_count")]
347 pub max_header_count: usize,
348
349 /// Maximum total size of all HTTP headers in bytes (default: 32 `KiB`).
350 ///
351 /// Requests whose combined header name+value bytes exceed this limit receive
352 /// 431 Request Header Fields Too Large. Prevents memory exhaustion from
353 /// oversized header values.
354 #[serde(default = "defaults::default_max_header_bytes")]
355 pub max_header_bytes: usize,
356
357 /// Per-request processing timeout in seconds (default: `None` — no timeout).
358 ///
359 /// When set, each HTTP request must complete within this many seconds or
360 /// the server returns **408 Request Timeout**. This is a defence-in-depth
361 /// measure against slow or runaway database queries.
362 ///
363 /// **Recommendation**: set to `60` for production deployments.
364 ///
365 /// # Example (TOML)
366 ///
367 /// ```toml
368 /// request_timeout_secs = 60
369 /// ```
370 #[serde(default)]
371 pub request_timeout_secs: Option<u64>,
372
373 /// Maximum byte length for a query string delivered via HTTP GET.
374 ///
375 /// GET queries are URL-encoded and passed as a query parameter. Very long
376 /// strings are either a `DoS` attempt or a sign that the caller should use
377 /// POST instead. Default: `100_000` (100 `KiB`).
378 ///
379 /// # Example (TOML)
380 ///
381 /// ```toml
382 /// max_get_query_bytes = 50000
383 /// ```
384 #[serde(default = "defaults::default_max_get_query_bytes")]
385 pub max_get_query_bytes: usize,
386
387 /// Rate limiting configuration for GraphQL requests.
388 ///
389 /// When configured, enables per-IP and per-user rate limiting with token bucket algorithm.
390 /// Defaults to enabled with sensible per-IP limits for security-by-default.
391 ///
392 /// # Example (TOML)
393 ///
394 /// ```toml
395 /// [rate_limiting]
396 /// enabled = true
397 /// rps_per_ip = 100 # 100 requests/second per IP
398 /// rps_per_user = 1000 # 1000 requests/second per authenticated user
399 /// burst_size = 500 # Allow bursts up to 500 requests
400 /// ```
401 #[serde(default)]
402 pub rate_limiting: Option<RateLimitConfig>,
403
404 /// Observer runtime configuration (optional, requires `observers` feature).
405 #[cfg(feature = "observers")]
406 #[serde(default)]
407 pub observers: Option<ObserverConfig>,
408
409 /// Connection pool pressure monitoring configuration.
410 ///
411 /// When `enabled = true`, the server spawns a background task that monitors
412 /// pool metrics and emits scaling recommendations via Prometheus metrics and
413 /// log lines. **The pool is not resized at runtime** — act on
414 /// `fraiseql_pool_tuning_*` events by adjusting `max_connections` and restarting.
415 ///
416 /// # Example (TOML)
417 ///
418 /// ```toml
419 /// [pool_tuning]
420 /// enabled = true
421 /// min_pool_size = 5
422 /// max_pool_size = 50
423 /// tuning_interval_ms = 30000
424 /// ```
425 #[serde(default)]
426 pub pool_tuning: Option<crate::config::pool_tuning::PoolPressureMonitorConfig>,
427
428 /// Admission control configuration.
429 ///
430 /// When set, enforces a maximum number of concurrent in-flight requests and
431 /// a maximum queue depth. Requests that exceed either limit receive
432 /// `503 Service Unavailable` immediately instead of stalling under load.
433 ///
434 /// # Example (TOML)
435 ///
436 /// ```toml
437 /// [admission_control]
438 /// max_concurrent = 500
439 /// max_queue_depth = 1000
440 /// ```
441 #[serde(default)]
442 pub admission_control: Option<AdmissionConfig>,
443
444 /// Security contact email for `/.well-known/security.txt` (RFC 9116).
445 ///
446 /// When set, the server exposes a `/.well-known/security.txt` endpoint
447 /// with this email address as the security contact. This helps security
448 /// researchers report vulnerabilities responsibly.
449 ///
450 /// # Example (TOML)
451 ///
452 /// ```toml
453 /// security_contact = "security@example.com"
454 /// ```
455 #[serde(default)]
456 pub security_contact: Option<String>,
457
458 /// Query validation overrides (depth and complexity limits).
459 ///
460 /// When present, these values take precedence over the limits baked into
461 /// the compiled schema, allowing operators to tune validation without
462 /// recompiling.
463 ///
464 /// # Example (TOML)
465 ///
466 /// ```toml
467 /// [validation]
468 /// max_query_depth = 15
469 /// max_query_complexity = 200
470 /// ```
471 #[serde(default)]
472 pub validation: Option<fraiseql_core::schema::ValidationConfig>,
473
474 /// Graceful shutdown drain timeout in seconds (default: 30).
475 ///
476 /// After a SIGTERM or Ctrl+C signal, the server stops accepting new connections and
477 /// waits for in-flight requests and background runtimes (observers) to finish.
478 /// If the drain takes longer than this value, the process logs a warning and exits
479 /// immediately instead of hanging indefinitely.
480 ///
481 /// Set this to match `terminationGracePeriodSeconds` in your Kubernetes pod spec
482 /// minus a small buffer (e.g., 25s when `terminationGracePeriodSeconds = 30`).
483 ///
484 /// Override with `FRAISEQL_SHUTDOWN_TIMEOUT_SECS`.
485 #[serde(default = "defaults::default_shutdown_timeout_secs")]
486 pub shutdown_timeout_secs: u64,
487}
488
489impl Default for ServerConfig {
490 fn default() -> Self {
491 Self {
492 schema_path: default_schema_path(),
493 database_url: default_database_url(),
494 bind_addr: default_bind_addr(),
495 #[cfg(feature = "arrow")]
496 flight_bind_addr: defaults::default_flight_bind_addr(),
497 cors_enabled: true,
498 cors_origins: Vec::new(),
499 compression_enabled: false,
500 tracing_enabled: true,
501 otlp_endpoint: None,
502 otlp_export_timeout_secs: defaults::default_otlp_timeout_secs(),
503 tracing_service_name: defaults::default_service_name(),
504 apq_enabled: true,
505 cache_enabled: true,
506 graphql_path: default_graphql_path(),
507 health_path: default_health_path(),
508 readiness_path: default_readiness_path(),
509 introspection_path: default_introspection_path(),
510 metrics_path: default_metrics_path(),
511 metrics_json_path: default_metrics_json_path(),
512 playground_path: default_playground_path(),
513 playground_enabled: false, // Disabled by default for security
514 playground_tool: PlaygroundTool::default(),
515 subscription_path: default_subscription_path(),
516 subscriptions_enabled: true,
517 metrics_enabled: false, // Disabled by default for security
518 metrics_token: None,
519 admin_api_enabled: false, // Disabled by default for security
520 admin_token: None,
521 admin_readonly_token: None,
522 introspection_enabled: false, // Disabled by default for security
523 introspection_require_auth: true, // Require auth when enabled
524 design_api_require_auth: true, // Require auth for design endpoints
525 pool_min_size: default_pool_min_size(),
526 pool_max_size: default_pool_max_size(),
527 pool_timeout_secs: default_pool_timeout(),
528 auth: None, // No auth by default
529 auth_hs256: None, // No HS256 auth by default
530 tls: None, // TLS disabled by default
531 database_tls: None, /* Database TLS disabled
532 * by default */
533 require_json_content_type: true, // CSRF protection
534 max_request_body_bytes: default_max_request_body_bytes(), // 1 MB
535 max_header_count: default_max_header_count(), // 100 headers
536 max_header_bytes: default_max_header_bytes(), // 32 KiB
537 rate_limiting: None, // Rate limiting uses defaults
538 #[cfg(feature = "observers")]
539 observers: None, // Observers disabled by default
540 pool_tuning: None, // Pool pressure monitoring disabled by default
541 admission_control: None, // Admission control disabled by default
542 security_contact: None, // No security.txt by default
543 validation: None, // Use compiled schema defaults
544 shutdown_timeout_secs: default_shutdown_timeout_secs(),
545 request_timeout_secs: None,
546 max_get_query_bytes: defaults::default_max_get_query_bytes(),
547 }
548 }
549}