Skip to main content

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