fraiseql_server/config/mod.rs
1//! Runtime configuration types for the FraiseQL server.
2//!
3//! Structs in this module are deserialized from `fraiseql.toml` (via the
4//! `loader` sub-module) or assembled from environment variables (via the
5//! `env` sub-module).
6//! Sub-modules contain configuration for specific subsystems such as CORS,
7//! metrics, rate limiting, and TLS.
8
9use std::{collections::HashMap, path::PathBuf};
10
11use serde::Deserialize;
12
13pub mod cors;
14pub mod env;
15pub mod error_sanitization;
16pub mod loader;
17pub mod metrics;
18pub mod pool_tuning;
19pub mod rate_limiting;
20#[cfg(test)]
21mod tests;
22pub mod tracing;
23pub mod validation;
24
25// Re-export config types
26pub use cors::CorsConfig;
27pub use error_sanitization::{ErrorSanitizationConfig, ErrorSanitizer};
28pub use metrics::{LatencyTargets, MetricsConfig, SloConfig};
29#[allow(deprecated)] // Reason: re-export deprecated alias for backwards compatibility
30pub use pool_tuning::{PoolPressureMonitorConfig, PoolTuningConfig};
31pub use rate_limiting::{BackpressureConfig, RateLimitRule, RateLimitingConfig};
32pub use tracing::TracingConfig;
33
34/// Root configuration structure loaded from `fraiseql.toml`.
35#[derive(Debug, Clone, Deserialize)]
36pub struct RuntimeConfig {
37 /// HTTP server binding, TLS, and connection-limit settings.
38 pub server: HttpServerConfig,
39 /// Primary database connection and pool settings.
40 pub database: DatabaseConfig,
41
42 /// Named webhook route configurations, keyed by route name.
43 #[serde(default)]
44 pub webhooks: HashMap<String, WebhookRouteConfig>,
45
46 /// Named file-upload route configurations, keyed by route name.
47 #[serde(default)]
48 pub files: HashMap<String, FileConfig>,
49
50 /// Optional JWT authentication and OAuth provider configuration.
51 #[serde(default)]
52 pub auth: Option<AuthConfig>,
53
54 /// Reserved: placeholder for future notification system configuration.
55 #[serde(default)]
56 pub notifications: Option<NotificationsConfig>,
57
58 /// Event observer configurations, keyed by observer name.
59 #[serde(default)]
60 pub observers: HashMap<String, ObserverConfig>,
61
62 /// Request interceptor chains, keyed by interceptor name.
63 #[serde(default)]
64 pub interceptors: HashMap<String, Vec<String>>,
65
66 /// Optional rate-limiting rules and backpressure thresholds.
67 #[serde(default)]
68 pub rate_limiting: Option<RateLimitingConfig>,
69
70 /// Optional CORS origin and header policy.
71 #[serde(default)]
72 pub cors: Option<CorsConfig>,
73
74 /// Optional Prometheus metrics and SLO tracking configuration.
75 #[serde(default)]
76 pub metrics: Option<MetricsConfig>,
77
78 /// Optional distributed-tracing (OTLP/Jaeger) configuration.
79 #[serde(default)]
80 pub tracing: Option<TracingConfig>,
81
82 /// Optional structured-logging configuration.
83 #[serde(default)]
84 pub logging: Option<LoggingConfig>,
85
86 /// Named object-storage backend configurations, keyed by storage name.
87 #[serde(default)]
88 pub storage: HashMap<String, StorageConfig>,
89
90 /// Reserved: placeholder for future search-indexing configuration.
91 #[serde(default)]
92 pub search: Option<SearchConfig>,
93
94 /// Reserved: placeholder for future advanced caching strategy configuration.
95 #[serde(default)]
96 pub cache: Option<CacheConfig>,
97
98 /// Reserved: placeholder for future job-queue configuration.
99 #[serde(default)]
100 pub queues: Option<QueueConfig>,
101
102 /// Reserved: placeholder for future real-time update configuration.
103 #[serde(default)]
104 pub realtime: Option<RealtimeConfig>,
105
106 /// Reserved: placeholder for future custom-endpoint configuration.
107 #[serde(default)]
108 pub custom_endpoints: Option<CustomEndpointsConfig>,
109
110 /// Graceful-shutdown timing and health-check endpoint paths.
111 #[serde(default)]
112 pub lifecycle: Option<LifecycleConfig>,
113}
114
115/// HTTP server binding configuration.
116#[derive(Debug, Clone, Deserialize)]
117pub struct HttpServerConfig {
118 /// TCP port to listen on. Default: `4000`.
119 #[serde(default = "default_port")]
120 pub port: u16,
121
122 /// Network interface to bind. Default: `"127.0.0.1"`.
123 #[serde(default = "default_host")]
124 pub host: String,
125
126 /// Number of async worker threads. `None` uses the Tokio default (number of CPU cores).
127 #[serde(default)]
128 pub workers: Option<usize>,
129
130 /// Optional TLS certificate and private key paths.
131 #[serde(default)]
132 pub tls: Option<TlsConfig>,
133
134 /// Optional per-request and concurrency limits.
135 #[serde(default)]
136 pub limits: Option<ServerLimitsConfig>,
137}
138
139const fn default_port() -> u16 {
140 4000
141}
142fn default_host() -> String {
143 "127.0.0.1".to_string()
144}
145
146/// TLS certificate and private key paths for HTTPS listeners.
147#[derive(Debug, Clone, Deserialize)]
148pub struct TlsConfig {
149 /// Path to the PEM-encoded TLS certificate (or certificate chain).
150 pub cert_file: PathBuf,
151 /// Path to the PEM-encoded private key corresponding to `cert_file`.
152 pub key_file: PathBuf,
153}
154
155/// Per-request body size and concurrency limits for the HTTP server.
156#[derive(Debug, Clone, Deserialize)]
157pub struct ServerLimitsConfig {
158 /// Maximum allowed request body size as a human-readable string (e.g. `"10MB"`). Default:
159 /// `"10MB"`.
160 #[serde(default = "default_max_request_size")]
161 pub max_request_size: String,
162
163 /// Maximum time to process a single request (e.g. `"30s"`). Default: `"30s"`.
164 #[serde(default = "default_request_timeout")]
165 pub request_timeout: String,
166
167 /// Maximum number of requests being processed simultaneously. Default: `1000`.
168 #[serde(default = "default_max_concurrent")]
169 pub max_concurrent_requests: usize,
170
171 /// Maximum number of requests waiting in the accept queue. Default: `5000`.
172 #[serde(default = "default_max_queue_depth")]
173 pub max_queue_depth: usize,
174}
175
176fn default_max_request_size() -> String {
177 "10MB".to_string()
178}
179fn default_request_timeout() -> String {
180 "30s".to_string()
181}
182const fn default_max_concurrent() -> usize {
183 1000
184}
185const fn default_max_queue_depth() -> usize {
186 5000
187}
188
189/// Primary database connection and connection-pool configuration.
190#[derive(Debug, Clone, Deserialize)]
191pub struct DatabaseConfig {
192 /// Name of the environment variable that holds the database connection URL.
193 pub url_env: String,
194
195 /// Maximum number of connections in the pool. Default: `10`.
196 #[serde(default = "default_pool_size")]
197 pub pool_size: u32,
198
199 /// How long to wait for an available connection before returning an error (e.g. `"5s"`).
200 #[serde(default)]
201 pub pool_timeout: Option<String>,
202
203 /// Per-query statement timeout sent to the database (e.g. `"30s"`).
204 #[serde(default)]
205 pub statement_timeout: Option<String>,
206
207 /// Optional read-replica pools used for load balancing SELECT queries.
208 #[serde(default)]
209 pub replicas: Vec<ReplicaConfig>,
210
211 /// How often to ping the database to verify liveness (e.g. `"60s"`).
212 #[serde(default)]
213 pub health_check_interval: Option<String>,
214}
215
216const fn default_pool_size() -> u32 {
217 10
218}
219
220/// Connection configuration for a single read replica.
221#[derive(Debug, Clone, Deserialize)]
222pub struct ReplicaConfig {
223 /// Name of the environment variable that holds this replica's connection URL.
224 pub url_env: String,
225
226 /// Relative weight for load-balancing SELECT queries across replicas. Default: `1`.
227 #[serde(default = "default_weight")]
228 pub weight: u32,
229}
230
231const fn default_weight() -> u32 {
232 1
233}
234
235/// Lifecycle configuration for graceful shutdown
236#[derive(Debug, Clone, Deserialize)]
237pub struct LifecycleConfig {
238 /// Time to wait for in-flight requests to complete
239 #[serde(default = "default_shutdown_timeout")]
240 pub shutdown_timeout: String,
241
242 /// Time to wait before starting shutdown (for load balancer deregistration)
243 #[serde(default = "default_shutdown_delay")]
244 pub shutdown_delay: String,
245
246 /// Health check endpoint path
247 #[serde(default = "default_health_path")]
248 pub health_path: String,
249
250 /// Readiness check endpoint path
251 #[serde(default = "default_ready_path")]
252 pub ready_path: String,
253}
254
255impl Default for LifecycleConfig {
256 fn default() -> Self {
257 Self {
258 shutdown_timeout: default_shutdown_timeout(),
259 shutdown_delay: default_shutdown_delay(),
260 health_path: default_health_path(),
261 ready_path: default_ready_path(),
262 }
263 }
264}
265
266fn default_shutdown_timeout() -> String {
267 "30s".to_string()
268}
269fn default_shutdown_delay() -> String {
270 "5s".to_string()
271}
272fn default_health_path() -> String {
273 "/health".to_string()
274}
275fn default_ready_path() -> String {
276 "/ready".to_string()
277}
278
279/// Configuration for a single incoming webhook route.
280#[derive(Debug, Clone, Deserialize)]
281pub struct WebhookRouteConfig {
282 /// Name of the environment variable that holds the webhook signing secret.
283 pub secret_env: String,
284 /// Webhook provider identifier (e.g. `"github"`, `"stripe"`).
285 pub provider: String,
286 /// URL path override; if absent, the route name is used as the path segment.
287 #[serde(default)]
288 pub path: Option<String>,
289}
290
291/// Configuration for a file-upload route.
292#[derive(Debug, Clone, Deserialize)]
293pub struct FileConfig {
294 /// Named storage backend (must match a key in `storage`).
295 pub storage: String,
296 /// Maximum upload size as a human-readable string (e.g. `"50MB"`).
297 pub max_size: String,
298 /// URL path prefix for upload and download endpoints.
299 #[serde(default)]
300 pub path: Option<String>,
301}
302
303/// JWT authentication and OAuth provider configuration.
304#[derive(Debug, Clone, Deserialize)]
305pub struct AuthConfig {
306 /// JWT signing secret configuration.
307 pub jwt: JwtConfig,
308 /// Named OAuth2/OIDC provider configurations.
309 #[serde(default)]
310 pub providers: HashMap<String, OAuthProviderConfig>,
311 /// Base URL for OAuth callback endpoints (e.g. `"https://api.example.com"`).
312 #[serde(default)]
313 pub callback_base_url: Option<String>,
314}
315
316/// JWT signing-secret configuration.
317#[derive(Debug, Clone, Deserialize)]
318pub struct JwtConfig {
319 /// Name of the environment variable that holds the JWT signing secret.
320 pub secret_env: String,
321}
322
323/// Configuration for a single OAuth2/OIDC provider.
324#[derive(Debug, Clone, Deserialize)]
325pub struct OAuthProviderConfig {
326 /// Well-known provider type identifier (e.g. `"auth0"`, `"github"`, `"google"`).
327 pub provider_type: String,
328 /// Name of the environment variable that holds the OAuth client ID.
329 pub client_id_env: String,
330 /// Name of the environment variable that holds the OAuth client secret.
331 pub client_secret_env: String,
332 /// OIDC issuer URL (required for providers that support OIDC discovery).
333 #[serde(default)]
334 pub issuer_url: Option<String>,
335}
336
337/// Reserved: placeholder for future notification system configuration.
338#[derive(Debug, Clone, Deserialize)]
339pub struct NotificationsConfig {}
340
341/// Configuration for a single event observer (entity-event → action).
342#[derive(Debug, Clone, Deserialize)]
343pub struct ObserverConfig {
344 /// GraphQL entity type name to watch (e.g. `"User"`).
345 pub entity: String,
346 /// List of mutation operation names that trigger this observer.
347 pub events: Vec<String>,
348 /// Ordered list of actions to execute when an observed event fires.
349 pub actions: Vec<ActionConfig>,
350}
351
352/// A single action within an observer pipeline.
353#[derive(Debug, Clone, Deserialize)]
354pub struct ActionConfig {
355 /// Action type identifier (e.g. `"webhook"`, `"email"`, `"queue"`).
356 #[serde(rename = "type")]
357 pub action_type: String,
358 /// Optional Jinja2-style template used to render the action payload.
359 #[serde(default)]
360 pub template: Option<String>,
361}
362
363// These types are now defined in their own modules and re-exported above
364
365/// Reserved: placeholder for future structured-logging configuration.
366#[derive(Debug, Clone, Deserialize)]
367pub struct LoggingConfig {}
368
369/// Configuration for a single object-storage backend.
370#[derive(Debug, Clone, Deserialize)]
371pub struct StorageConfig {
372 /// Storage backend identifier (e.g. `"s3"`, `"gcs"`, `"local"`).
373 pub backend: String,
374 /// Bucket or container name (required for cloud backends).
375 #[serde(default)]
376 pub bucket: Option<String>,
377 /// Local filesystem path (used by the `"local"` backend).
378 #[serde(default)]
379 pub path: Option<String>,
380 /// Cloud region (e.g. `"eu-west-1"` for AWS, `"fr-par"` for Scaleway).
381 #[serde(default)]
382 pub region: Option<String>,
383 /// Custom endpoint URL (for S3-compatible providers or local development).
384 #[serde(default)]
385 pub endpoint: Option<String>,
386 /// GCP project ID (used by the `"gcs"` backend).
387 #[serde(default)]
388 pub project_id: Option<String>,
389 /// Azure storage account name (used by the `"azure"` backend).
390 #[serde(default)]
391 pub account_name: Option<String>,
392}
393
394/// Reserved: placeholder for future full-text search indexing configuration.
395#[derive(Debug, Clone, Deserialize)]
396pub struct SearchConfig {}
397
398/// Reserved: placeholder for future advanced query-result caching configuration.
399#[derive(Debug, Clone, Deserialize)]
400pub struct CacheConfig {}
401
402/// Reserved: placeholder for future background job-queue configuration.
403#[derive(Debug, Clone, Deserialize)]
404pub struct QueueConfig {}
405
406/// Reserved: placeholder for future real-time subscription update configuration.
407#[derive(Debug, Clone, Deserialize)]
408pub struct RealtimeConfig {}
409
410/// Reserved: placeholder for future custom HTTP endpoint configuration.
411#[derive(Debug, Clone, Deserialize)]
412pub struct CustomEndpointsConfig {}