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 storage;
15pub mod tls;
16
17#[cfg(test)]
18mod tests;
19
20use std::{collections::HashMap, net::SocketAddr, path::PathBuf};
21
22use defaults::{
23 default_bind_addr, default_database_url, default_graphql_path, default_health_path,
24 default_introspection_path, default_max_header_bytes, default_max_header_count,
25 default_max_request_body_bytes, default_metrics_json_path, default_metrics_path,
26 default_playground_path, default_pool_max_size, default_pool_min_size, default_pool_timeout,
27 default_readiness_path, default_schema_path, default_shutdown_timeout_secs,
28 default_subscription_path,
29};
30use fraiseql_core::security::OidcConfig;
31pub use hs256::Hs256Config;
32pub use observers::AdmissionConfig;
33#[cfg(feature = "observers")]
34pub use observers::{ObserverConfig, ObserverPoolConfig, ObserverRuntimeSettings};
35use serde::{Deserialize, Serialize};
36pub use storage::{ResolvedStorage, build_storage_state, resolve_storage_section};
37pub use tls::{DatabaseTlsConfig, PlaygroundTool, TlsServerConfig};
38
39use crate::middleware::RateLimitConfig;
40
41/// Server configuration.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ServerConfig {
44 /// Path to compiled schema JSON file.
45 #[serde(default = "defaults::default_schema_path")]
46 pub schema_path: PathBuf,
47
48 /// Fail boot if any declared `sql_source` (query view / mutation function) is
49 /// not backed by the database (#487).
50 ///
51 /// Default `false` — the boot path is unchanged. Postgres-only. The
52 /// `--validate-sql-sources` CLI flag and the `FRAISEQL_VALIDATE_SQL_SOURCES`
53 /// environment variable both override this (env/flag win over the config key).
54 #[serde(default)]
55 pub validate_sql_sources: bool,
56
57 /// Database connection URL (PostgreSQL, MySQL, SQLite, SQL Server).
58 #[serde(default = "defaults::default_database_url")]
59 pub database_url: String,
60
61 /// Server bind address.
62 #[serde(default = "defaults::default_bind_addr")]
63 pub bind_addr: SocketAddr,
64
65 /// Arrow Flight gRPC bind address (requires `arrow` feature).
66 ///
67 /// Defaults to `0.0.0.0:50051`. Override with `FRAISEQL_FLIGHT_BIND_ADDR`
68 /// environment variable or this field in the config file.
69 #[cfg(feature = "arrow")]
70 #[serde(default = "defaults::default_flight_bind_addr")]
71 pub flight_bind_addr: SocketAddr,
72
73 /// Enable CORS.
74 #[serde(default = "defaults::default_true")]
75 pub cors_enabled: bool,
76
77 /// CORS allowed origins (if empty, allows all).
78 #[serde(default)]
79 pub cors_origins: Vec<String>,
80
81 /// Enable framework-level response compression.
82 ///
83 /// Defaults to `false`. In production FraiseQL is typically deployed
84 /// behind a reverse proxy (Nginx, Caddy, cloud load balancer) that
85 /// handles compression more efficiently (brotli, shared across upstreams,
86 /// cacheable). Enable this only for single-binary / no-proxy deployments.
87 #[serde(default = "defaults::default_false")]
88 pub compression_enabled: bool,
89
90 /// Enable request tracing.
91 #[serde(default = "defaults::default_true")]
92 pub tracing_enabled: bool,
93
94 /// OTLP exporter endpoint for distributed tracing.
95 ///
96 /// When set (e.g. `"http://otel-collector:4317"`), the server initializes an
97 /// `OpenTelemetry` OTLP exporter. When `None`, the `OTEL_EXPORTER_OTLP_ENDPOINT`
98 /// environment variable is checked as a fallback. If neither is set, no OTLP
99 /// export occurs (zero overhead).
100 #[serde(default)]
101 pub otlp_endpoint: Option<String>,
102
103 /// OTLP exporter timeout in seconds (default: 10).
104 #[serde(default = "defaults::default_otlp_timeout_secs")]
105 pub otlp_export_timeout_secs: u64,
106
107 /// Service name for distributed tracing (default: `"fraiseql"`).
108 #[serde(default = "defaults::default_service_name")]
109 pub tracing_service_name: String,
110
111 /// Enable APQ (Automatic Persisted Queries).
112 #[serde(default = "defaults::default_true")]
113 pub apq_enabled: bool,
114
115 /// Enable query caching.
116 #[serde(default = "defaults::default_true")]
117 pub cache_enabled: bool,
118
119 /// GraphQL endpoint path.
120 #[serde(default = "defaults::default_graphql_path")]
121 pub graphql_path: String,
122
123 /// Health check endpoint path (liveness probe).
124 ///
125 /// Returns 200 as long as the process is alive, 503 if the database is down.
126 #[serde(default = "defaults::default_health_path")]
127 pub health_path: String,
128
129 /// Readiness probe endpoint path.
130 ///
131 /// Returns 200 when the server is ready to serve traffic (database reachable),
132 /// 503 otherwise. Kubernetes `readinessProbe` should point here.
133 #[serde(default = "defaults::default_readiness_path")]
134 pub readiness_path: String,
135
136 /// Introspection endpoint path.
137 #[serde(default = "defaults::default_introspection_path")]
138 pub introspection_path: String,
139
140 /// Metrics endpoint path (Prometheus format).
141 #[serde(default = "defaults::default_metrics_path")]
142 pub metrics_path: String,
143
144 /// Metrics JSON endpoint path.
145 #[serde(default = "defaults::default_metrics_json_path")]
146 pub metrics_json_path: String,
147
148 /// Playground (GraphQL IDE) endpoint path.
149 #[serde(default = "defaults::default_playground_path")]
150 pub playground_path: String,
151
152 /// Enable GraphQL playground/IDE (default: false for production safety).
153 ///
154 /// When enabled, serves a GraphQL IDE (`GraphiQL` or Apollo Sandbox)
155 /// at the configured `playground_path`.
156 ///
157 /// **Security**: Disabled by default for production safety. Set to true for development
158 /// environments only. The playground exposes schema information and can be a
159 /// reconnaissance vector for attackers.
160 #[serde(default)]
161 pub playground_enabled: bool,
162
163 /// Which GraphQL IDE to use.
164 ///
165 /// - `graphiql`: The classic GraphQL IDE (default)
166 /// - `apollo-sandbox`: Apollo's embeddable sandbox
167 #[serde(default)]
168 pub playground_tool: PlaygroundTool,
169
170 /// `WebSocket` endpoint path for GraphQL subscriptions.
171 #[serde(default = "defaults::default_subscription_path")]
172 pub subscription_path: String,
173
174 /// Enable GraphQL subscriptions over `WebSocket`.
175 ///
176 /// When enabled, provides graphql-ws (graphql-transport-ws) protocol
177 /// support for real-time subscription events.
178 #[serde(default = "defaults::default_true")]
179 pub subscriptions_enabled: bool,
180
181 /// Enable metrics endpoints.
182 ///
183 /// **Security**: Disabled by default for production safety.
184 /// When enabled, requires `metrics_token` to be set for authentication.
185 #[serde(default)]
186 pub metrics_enabled: bool,
187
188 /// Bearer token for metrics endpoint authentication.
189 ///
190 /// Required when `metrics_enabled` is true. Requests must include:
191 /// `Authorization: Bearer <token>`
192 ///
193 /// **Security**: Use a strong, random token (e.g., 32+ characters).
194 #[serde(default)]
195 pub metrics_token: Option<String>,
196
197 /// Enable admin API endpoints (default: false for production safety).
198 ///
199 /// **Security**: Disabled by default. When enabled, requires `admin_token` to be set.
200 /// Admin endpoints allow schema reloading, cache management, and config inspection.
201 #[serde(default)]
202 pub admin_api_enabled: bool,
203
204 /// Bearer token for admin API authentication.
205 ///
206 /// Required when `admin_api_enabled` is true. Requests must include:
207 /// `Authorization: Bearer <token>`
208 ///
209 /// **Security**: Use a strong, random token (minimum 32 characters).
210 /// This token grants access to **destructive** admin operations:
211 /// `reload-schema`, `cache/clear`.
212 ///
213 /// If `admin_readonly_token` is set, this token is restricted to write
214 /// operations only. If `admin_readonly_token` is not set, this token
215 /// also grants access to read-only endpoints (backwards-compatible).
216 #[serde(default)]
217 pub admin_token: Option<String>,
218
219 /// Optional separate bearer token for read-only admin operations.
220 ///
221 /// When set, restricts `admin_token` to destructive operations only
222 /// (`reload-schema`, `cache/clear`) and uses this token for read-only
223 /// endpoints (`config`, `cache/stats`, `explain`, `grafana-dashboard`).
224 ///
225 /// Operators and monitoring tools can use this token without gaining
226 /// the ability to modify server state or reload the schema.
227 ///
228 /// **Security**: Must be different from `admin_token` and at least 32
229 /// characters. Requires `admin_api_enabled = true` and `admin_token` set.
230 #[serde(default)]
231 pub admin_readonly_token: Option<String>,
232
233 /// Enable introspection endpoint (default: false for production safety).
234 ///
235 /// **Security**: Disabled by default. When enabled, the introspection endpoint
236 /// exposes the complete GraphQL schema structure. Combined with `introspection_require_auth`,
237 /// you can optionally protect it with OIDC authentication.
238 #[serde(default)]
239 pub introspection_enabled: bool,
240
241 /// Require authentication for introspection endpoint (default: true).
242 ///
243 /// When true and OIDC is configured, introspection requires same auth as GraphQL endpoint.
244 /// When false, introspection is publicly accessible (use only in development).
245 #[serde(default = "defaults::default_true")]
246 pub introspection_require_auth: bool,
247
248 /// Require authentication for the schema metadata endpoint (default: None).
249 ///
250 /// When `Some(true)`, the `/api/v1/schema/metadata` endpoint requires OIDC auth
251 /// independently of introspection. When `Some(false)`, metadata is publicly
252 /// accessible regardless of introspection auth. When `None` (default), falls
253 /// back to `introspection_require_auth` for backwards compatibility.
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub metadata_require_auth: Option<bool>,
256
257 /// Require authentication for schema export endpoints (default: None).
258 ///
259 /// Controls `/api/v1/schema.graphql` and `/api/v1/schema.json` independently of
260 /// introspection auth. When `Some(true)`, schema export requires OIDC auth. When
261 /// `Some(false)`, schema export is publicly accessible. When `None` (default),
262 /// falls back to `introspection_require_auth` for backwards compatibility.
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub schema_export_require_auth: Option<bool>,
265
266 /// Require authentication for the GraphQL Playground endpoint (default: None).
267 ///
268 /// Controls the playground independently of introspection auth. When `Some(true)`,
269 /// the playground requires OIDC auth. When `Some(false)`, the playground is publicly
270 /// accessible. When `None` (default), falls back to `introspection_require_auth`
271 /// for backwards compatibility.
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub playground_require_auth: Option<bool>,
274
275 /// Require authentication for the `WebSocket` subscription endpoint (default: None).
276 ///
277 /// Controls the `/subscriptions` endpoint independently of introspection auth.
278 /// When `Some(true)`, the subscription endpoint requires OIDC auth. When `Some(false)`,
279 /// subscriptions are publicly accessible. When `None` (default), falls back to
280 /// `introspection_require_auth` for backwards compatibility.
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub subscription_require_auth: Option<bool>,
283
284 /// Require authentication for design audit API endpoints (default: true).
285 ///
286 /// Design audit endpoints expose system architecture and optimization opportunities.
287 /// When true and OIDC is configured, design endpoints require same auth as GraphQL endpoint.
288 /// When false, design endpoints are publicly accessible (use only in development).
289 #[serde(default = "defaults::default_true")]
290 pub design_api_require_auth: bool,
291
292 /// Database connection pool minimum size.
293 #[serde(default = "defaults::default_pool_min_size")]
294 pub pool_min_size: usize,
295
296 /// Database connection pool maximum size.
297 #[serde(default = "defaults::default_pool_max_size")]
298 pub pool_max_size: usize,
299
300 /// Database connection pool timeout in seconds.
301 #[serde(default = "defaults::default_pool_timeout")]
302 pub pool_timeout_secs: u64,
303
304 /// OIDC authentication configuration (optional).
305 ///
306 /// When set, enables JWT authentication using OIDC discovery.
307 /// Supports Auth0, Keycloak, Okta, Cognito, Azure AD, and any
308 /// OIDC-compliant provider.
309 ///
310 /// # Example (TOML)
311 ///
312 /// ```toml
313 /// [auth]
314 /// issuer = "https://your-tenant.auth0.com/"
315 /// audience = "your-api-identifier"
316 /// ```
317 #[serde(default)]
318 pub auth: Option<OidcConfig>,
319
320 /// HS256 symmetric-key authentication (optional).
321 ///
322 /// Alternative to `auth` (OIDC) for integration testing and internal
323 /// service-to-service scenarios. Mutually exclusive with `auth`.
324 ///
325 /// Validation is fully local — no discovery endpoint, no JWKS fetch.
326 /// Not recommended for public-facing production.
327 ///
328 /// # Example (TOML)
329 ///
330 /// ```toml
331 /// [auth_hs256]
332 /// secret_env = "FRAISEQL_HS256_SECRET"
333 /// issuer = "my-test-suite"
334 /// audience = "my-api"
335 /// ```
336 #[serde(default)]
337 pub auth_hs256: Option<Hs256Config>,
338
339 /// Name of the environment variable holding the server HMAC secret.
340 ///
341 /// When set, the per-dispatch idempotency token surfaced to functions is
342 /// HMAC-signed with a subkey derived from this secret, making it unforgeable —
343 /// required before it is exposed externally as a VERP delivery-tracking
344 /// Return-Path. Unset → the token is an unsigned digest (the zero-config
345 /// default). The secret must be **stable across restarts and shared across
346 /// instances** (a per-process random value would break resume + multi-instance
347 /// idempotency); resolved from the environment at startup, never the config file.
348 ///
349 /// ```toml
350 /// hmac_secret_env = "FRAISEQL_HMAC_SECRET"
351 /// ```
352 #[serde(default)]
353 pub hmac_secret_env: Option<String>,
354
355 /// TLS/SSL configuration for HTTPS and encrypted connections.
356 ///
357 /// When set, enables TLS enforcement for HTTP/gRPC endpoints and
358 /// optionally requires mutual TLS (mTLS) for client certificates.
359 ///
360 /// # Example (TOML)
361 ///
362 /// ```toml
363 /// [tls]
364 /// enabled = true
365 /// cert_path = "/etc/fraiseql/cert.pem"
366 /// key_path = "/etc/fraiseql/key.pem"
367 /// require_client_cert = false
368 /// min_version = "1.2" # "1.2" or "1.3"
369 /// ```
370 #[serde(default)]
371 pub tls: Option<TlsServerConfig>,
372
373 /// Database TLS configuration.
374 ///
375 /// Enables TLS for database connections and configures
376 /// per-database TLS settings (PostgreSQL, Redis, `ClickHouse`, etc.).
377 ///
378 /// # Example (TOML)
379 ///
380 /// ```toml
381 /// [database_tls]
382 /// postgres_ssl_mode = "require" # disable, allow, prefer, require, verify-ca, verify-full
383 /// redis_ssl = true # Use rediss:// protocol
384 /// clickhouse_https = true # Use HTTPS
385 /// elasticsearch_https = true # Use HTTPS
386 /// verify_certificates = true # Verify server certificates
387 /// ```
388 #[serde(default)]
389 pub database_tls: Option<DatabaseTlsConfig>,
390
391 /// Require `Content-Type: application/json` on POST requests (default: true).
392 ///
393 /// CSRF protection: rejects POST requests with non-JSON Content-Type
394 /// (e.g. `text/plain`, `application/x-www-form-urlencoded`) with 415.
395 #[serde(default = "defaults::default_true")]
396 pub require_json_content_type: bool,
397
398 /// Maximum request body size in bytes (default: 1 MB).
399 ///
400 /// Requests exceeding this limit receive 413 Payload Too Large.
401 /// Set to 0 to use axum's default (no limit).
402 #[serde(default = "defaults::default_max_request_body_bytes")]
403 pub max_request_body_bytes: usize,
404
405 /// Maximum number of HTTP headers per request (default: 100).
406 ///
407 /// Requests with more headers than this limit receive 431 Request Header Fields Too Large.
408 /// Prevents header-flooding `DoS` attacks that exhaust memory.
409 #[serde(default = "defaults::default_max_header_count")]
410 pub max_header_count: usize,
411
412 /// Maximum total size of all HTTP headers in bytes (default: 32 `KiB`).
413 ///
414 /// Requests whose combined header name+value bytes exceed this limit receive
415 /// 431 Request Header Fields Too Large. Prevents memory exhaustion from
416 /// oversized header values.
417 #[serde(default = "defaults::default_max_header_bytes")]
418 pub max_header_bytes: usize,
419
420 /// Per-request processing timeout in seconds (default: `None` — no timeout).
421 ///
422 /// When set, each HTTP request must complete within this many seconds or
423 /// the server returns **408 Request Timeout**. This is a defence-in-depth
424 /// measure against slow or runaway database queries.
425 ///
426 /// **Recommendation**: set to `60` for production deployments.
427 ///
428 /// # Example (TOML)
429 ///
430 /// ```toml
431 /// request_timeout_secs = 60
432 /// ```
433 #[serde(default)]
434 pub request_timeout_secs: Option<u64>,
435
436 /// Maximum byte length for a query string delivered via HTTP GET.
437 ///
438 /// GET queries are URL-encoded and passed as a query parameter. Very long
439 /// strings are either a `DoS` attempt or a sign that the caller should use
440 /// POST instead. Default: `100_000` (100 `KiB`).
441 ///
442 /// # Example (TOML)
443 ///
444 /// ```toml
445 /// max_get_query_bytes = 50000
446 /// ```
447 #[serde(default = "defaults::default_max_get_query_bytes")]
448 pub max_get_query_bytes: usize,
449
450 /// Rate limiting configuration for GraphQL requests.
451 ///
452 /// When configured, enables per-IP and per-user rate limiting with token bucket algorithm.
453 /// Defaults to enabled with sensible per-IP limits for security-by-default.
454 ///
455 /// # Example (TOML)
456 ///
457 /// ```toml
458 /// [rate_limiting]
459 /// enabled = true
460 /// rps_per_ip = 100 # 100 requests/second per IP
461 /// rps_per_user = 1000 # 1000 requests/second per authenticated user
462 /// burst_size = 500 # Allow bursts up to 500 requests
463 /// ```
464 #[serde(default)]
465 pub rate_limiting: Option<RateLimitConfig>,
466
467 /// Observer runtime configuration (optional, requires `observers` feature).
468 #[cfg(feature = "observers")]
469 #[serde(default)]
470 pub observers: Option<ObserverConfig>,
471
472 /// Scheduled-ingress source scheduler configuration (optional, requires the
473 /// `sources` feature). The source *definitions* live in the compiled schema;
474 /// this `[sources]` section tunes the runtime (global on/off, connector SSRF
475 /// allowlist).
476 ///
477 /// Boxed to keep `ServerConfig` small: this section is rarely set and read once
478 /// at startup, so it does not belong inline in a struct that sits on the
479 /// request-handling futures (an inline copy tips borderline futures past
480 /// clippy's `large_futures` stack budget).
481 #[cfg(feature = "sources")]
482 #[serde(default)]
483 pub sources: Option<Box<SourcesConfig>>,
484
485 /// Connection pool pressure monitoring configuration.
486 ///
487 /// When `enabled = true`, the server spawns a background task that monitors
488 /// pool metrics and emits scaling recommendations via Prometheus metrics and
489 /// log lines. **The pool is not resized at runtime** — act on
490 /// `fraiseql_pool_tuning_*` events by adjusting `max_connections` and restarting.
491 ///
492 /// # Example (TOML)
493 ///
494 /// ```toml
495 /// [pool_tuning]
496 /// enabled = true
497 /// min_pool_size = 5
498 /// max_pool_size = 50
499 /// tuning_interval_ms = 30000
500 /// ```
501 #[serde(default)]
502 pub pool_tuning: Option<crate::config::pool_tuning::PoolPressureMonitorConfig>,
503
504 /// Admission control configuration.
505 ///
506 /// When set, enforces a maximum number of concurrent in-flight requests and
507 /// a maximum queue depth. Requests that exceed either limit receive
508 /// `503 Service Unavailable` immediately instead of stalling under load.
509 ///
510 /// # Example (TOML)
511 ///
512 /// ```toml
513 /// [admission_control]
514 /// max_concurrent = 500
515 /// max_queue_depth = 1000
516 /// ```
517 #[serde(default)]
518 pub admission_control: Option<AdmissionConfig>,
519
520 /// Security contact email for `/.well-known/security.txt` (RFC 9116).
521 ///
522 /// When set, the server exposes a `/.well-known/security.txt` endpoint
523 /// with this email address as the security contact. This helps security
524 /// researchers report vulnerabilities responsibly.
525 ///
526 /// # Example (TOML)
527 ///
528 /// ```toml
529 /// security_contact = "security@example.com"
530 /// ```
531 #[serde(default)]
532 pub security_contact: Option<String>,
533
534 /// Query validation overrides (depth and complexity limits).
535 ///
536 /// When present, these values take precedence over the limits baked into
537 /// the compiled schema, allowing operators to tune validation without
538 /// recompiling.
539 ///
540 /// # Example (TOML)
541 ///
542 /// ```toml
543 /// [validation]
544 /// max_query_depth = 15
545 /// max_query_complexity = 200
546 /// ```
547 #[serde(default)]
548 pub validation: Option<fraiseql_core::schema::ValidationConfig>,
549
550 /// Maximum failed admin bearer auth attempts per IP within a 60-second
551 /// window before the IP is blocked with 429 Too Many Requests (default: 10).
552 ///
553 /// Set to `0` to disable brute-force protection entirely (not recommended).
554 ///
555 /// # Example (TOML)
556 ///
557 /// ```toml
558 /// admin_auth_max_failures = 5
559 /// ```
560 #[serde(default = "defaults::default_admin_auth_max_failures")]
561 pub admin_auth_max_failures: u32,
562
563 /// Bearer token protecting the storage REST API (`/storage/v1/`).
564 ///
565 /// When set, all requests to storage endpoints must include an
566 /// `Authorization: Bearer <token>` header that matches this value. Requests
567 /// without a valid token receive **401 Unauthorized**.
568 ///
569 /// **Security**: This token protects *all* storage operations (upload, download,
570 /// delete, presigned URL). Use a strong random string (minimum 32 characters).
571 /// Omit the field (or set `None`) to leave storage endpoints open — appropriate
572 /// only in development or when the storage API is behind a trusted network boundary.
573 ///
574 /// # Example (TOML)
575 ///
576 /// ```toml
577 /// storage_token = "your-strong-random-token-here"
578 /// ```
579 #[serde(default)]
580 pub storage_token: Option<String>,
581
582 /// Graceful shutdown drain timeout in seconds (default: 30).
583 ///
584 /// After a SIGTERM or Ctrl+C signal, the server stops accepting new connections and
585 /// waits for in-flight requests and background runtimes (observers) to finish.
586 /// If the drain takes longer than this value, the process logs a warning and exits
587 /// immediately instead of hanging indefinitely.
588 ///
589 /// Set this to match `terminationGracePeriodSeconds` in your Kubernetes pod spec
590 /// minus a small buffer (e.g., 25s when `terminationGracePeriodSeconds = 30`).
591 ///
592 /// Override with `FRAISEQL_SHUTDOWN_TIMEOUT_SECS`.
593 #[serde(default = "defaults::default_shutdown_timeout_secs")]
594 pub shutdown_timeout_secs: u64,
595
596 /// Usage counter persistence configuration (optional).
597 ///
598 /// When set, mutation usage counters are periodically flushed to PostgreSQL
599 /// and restored on server startup. Requires a PostgreSQL database URL.
600 ///
601 /// ```toml
602 /// [usage]
603 /// flush_interval_secs = 60
604 /// ```
605 ///
606 /// When absent (default), counters are in-memory only and reset on restart.
607 #[serde(default)]
608 pub usage: Option<crate::config::UsagePersistenceConfig>,
609
610 /// Named object-storage backend configurations, keyed by storage name.
611 ///
612 /// Each `[storage.<name>]` section is wired into a mounted `/storage/v1/*`
613 /// route group on startup (PostgreSQL only — the object-metadata repository
614 /// requires a `sqlx::PgPool`). The section name is the logical bucket name in
615 /// the URL path (`/storage/v1/object/<name>/<key>`). See
616 /// [`StorageSectionConfig`].
617 ///
618 /// **v1 supports a single section.** Configuring more than one
619 /// `[storage.<name>]` is a startup error (multiplexing several physical
620 /// backends behind one route group is a planned follow-up).
621 ///
622 /// # Example (TOML)
623 ///
624 /// ```toml
625 /// [storage.uploads]
626 /// backend = "local"
627 /// path = "/var/lib/fraiseql/uploads"
628 /// access = "public_read"
629 /// ```
630 #[serde(default)]
631 pub storage: HashMap<String, StorageSectionConfig>,
632
633 /// Named file-upload route configurations, keyed by route name.
634 ///
635 /// **Not yet wired into the binary** (tracked as a `[storage]` follow-up).
636 /// The section is parsed only so the server can warn at startup rather than
637 /// silently dropping it. See [`FileSectionConfig`].
638 #[serde(default)]
639 pub files: HashMap<String, FileSectionConfig>,
640
641 /// Inbound webhook receiver routes (`[webhooks.<name>]`), keyed by route name.
642 ///
643 /// Each entry mounts `POST /webhooks/<name>`: the delivery is signature-verified
644 /// (per `provider`, secret from `secret_env`), normalized to an `InboundMessage`,
645 /// and persisted onto the durable spine. Requires the `inbound` feature and a
646 /// PostgreSQL pool; empty by default.
647 #[cfg(feature = "inbound")]
648 #[serde(default)]
649 pub webhooks: HashMap<String, crate::config::WebhookRouteConfig>,
650
651 /// Connected mailbox accounts (`[mailbox.<name>]`), keyed by account name.
652 ///
653 /// Each account carries an optional poll-IMAP receive half
654 /// (`[mailbox.<name>.imap]`) — a background poll worker that fetches new
655 /// messages by UID watermark, normalizes their MIME to an `InboundMessage` on
656 /// the durable spine, and fires `after:ingest:email` functions — and (via the
657 /// hardening `send_email` transport) an SMTP send half. Requires the
658 /// `inbound-email` feature and a PostgreSQL pool; empty by default.
659 #[cfg(feature = "inbound-email")]
660 #[serde(default)]
661 pub mailbox: HashMap<String, crate::inbound::email::MailboxConfig>,
662
663 /// Delivery-feedback send policy (`[send]`).
664 ///
665 /// Governs how the correlation step reacts to inbound bounces / challenges /
666 /// replies — currently the challenge-suppression threshold
667 /// (`challenge_suppress_after`, default 2). Requires the `inbound-email`
668 /// feature; defaults apply when the section is absent.
669 #[cfg(feature = "inbound-email")]
670 #[serde(default)]
671 pub send: crate::inbound::email::SendSettings,
672
673 /// Multi-tenant executor runtime configuration.
674 ///
675 /// Off by default. Enable with `[tenancy.runtime] enabled = true` to mount the
676 /// multi-tenant runtime in the off-the-shelf binary: the per-tenant executor
677 /// registry, `X-Tenant-ID` / JWT `tenant_id` / Host dispatch, and the
678 /// `/api/v1/admin/tenants/*` lifecycle API. Runtime tenant provisioning
679 /// (registering a tenant with its own connection) is PostgreSQL-only.
680 ///
681 /// ```toml
682 /// [tenancy.runtime]
683 /// enabled = true
684 /// ```
685 #[serde(default)]
686 pub tenancy: TenancyServerConfig,
687
688 /// Enriched-identity resolution (#539): `[identity.enrichment]` /
689 /// `[identity.sender]`. Top-level (not under `[auth]`) so it applies under
690 /// any auth mode — HS256/OIDC parity by construction. Gated on `auth`
691 /// because enrichment requires an authenticated subject and the auth DB pool.
692 #[cfg(feature = "auth")]
693 #[serde(default)]
694 pub identity: Option<crate::identity::IdentityConfig>,
695}
696
697/// A single `[storage.<name>]` configuration section.
698///
699/// Combines the storage *backend* connection settings (mapped to
700/// [`fraiseql_storage::config::StorageConfig`] and passed to
701/// `fraiseql_storage::create_backend`) with the optional *logical-bucket* access
702/// policy (mapped to `fraiseql_storage::config::BucketConfig`). The section name
703/// becomes the logical bucket name in the URL path.
704///
705/// `[sources]` — runtime configuration for the scheduled-ingress source scheduler
706/// (#573, requires the `sources` feature).
707///
708/// The source *definitions* (name, schedule, function, `run_as`) come from the
709/// compiled schema; this section is operator-facing runtime tuning. Both fields are
710/// overridable by environment variables (env > TOML > default) so production can
711/// tune without recompiling:
712///
713/// - `FRAISEQL_SOURCES_ENABLED` — global on/off (`false`/`0`/`no`/`off` disables).
714/// - `FRAISEQL_SOURCES_ALLOWED_DOMAINS` — comma-separated SSRF allowlist.
715#[cfg(feature = "sources")]
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct SourcesConfig {
718 /// Global on/off for the source scheduler. Per-source `enabled` in the compiled
719 /// schema still applies; this disables the whole scheduler without recompiling.
720 /// Default: `true`.
721 #[serde(default = "defaults::default_true")]
722 pub enabled: bool,
723
724 /// SSRF allowlist (glob patterns) for source connectors' outbound fetches.
725 /// Deny-by-default: empty permits no outbound host.
726 #[serde(default)]
727 pub allowed_domains: Vec<String>,
728
729 /// Log each firing's trigger payload at debug (default: `false`).
730 ///
731 /// Off-by-default mirrors the observer `log_payloads` gate. A source's trigger
732 /// payload carries only schedule context (the external data the connector fetches
733 /// never reaches the poller), so the risk is low — but payload logging stays an
734 /// operator opt-in for a uniform PII stance.
735 #[serde(default)]
736 pub log_payloads: bool,
737}
738
739#[cfg(feature = "sources")]
740impl Default for SourcesConfig {
741 fn default() -> Self {
742 Self {
743 enabled: true,
744 allowed_domains: Vec::new(),
745 log_payloads: false,
746 }
747 }
748}
749
750/// The connection fields mirror [`fraiseql_storage::config::StorageConfig`].
751///
752/// The policy fields (`access`, `max_object_bytes`, `allowed_mime_types`,
753/// `serve_inline`) are optional and default to a private, force-download bucket.
754#[derive(Debug, Clone, Serialize, Deserialize)]
755pub struct StorageSectionConfig {
756 /// Backend type: `"local"`, `"s3"` (and S3-compatible providers `"hetzner"`,
757 /// `"scaleway"`, `"ovh"`, `"exoscale"`, `"backblaze"`, `"r2"`), `"gcs"`,
758 /// `"azure"`. Non-local backends require the matching Cargo feature
759 /// (`aws-s3`, `gcs`, `azure-blob`) to be compiled in.
760 pub backend: String,
761
762 /// Filesystem path for the `local` backend.
763 #[serde(default)]
764 pub path: Option<String>,
765
766 /// Physical bucket name for S3/GCS/Azure backends.
767 #[serde(default)]
768 pub bucket: Option<String>,
769
770 /// Region for S3-compatible backends.
771 #[serde(default)]
772 pub region: Option<String>,
773
774 /// Custom endpoint URL for S3-compatible services and cloud emulators.
775 #[serde(default)]
776 pub endpoint: Option<String>,
777
778 /// GCP project ID for the GCS backend.
779 #[serde(default)]
780 pub project_id: Option<String>,
781
782 /// Azure storage account name.
783 #[serde(default)]
784 pub account_name: Option<String>,
785
786 /// Access policy for the logical bucket: `"private"` (default) or
787 /// `"public_read"`. An unrecognised value is a startup error.
788 #[serde(default)]
789 pub access: Option<String>,
790
791 /// Maximum object size in bytes for the logical bucket (None = unlimited,
792 /// subject to the route-wide body limit).
793 #[serde(default)]
794 pub max_object_bytes: Option<u64>,
795
796 /// Allowed MIME types for uploads (None = any). Supports `image/*`-style
797 /// wildcards.
798 #[serde(default)]
799 pub allowed_mime_types: Option<Vec<String>>,
800
801 /// Serve downloads with `Content-Disposition: inline` instead of the default
802 /// `attachment`. Active-content types (`text/html`, `image/svg+xml`, …) are
803 /// always served as `attachment` regardless of this flag.
804 #[serde(default)]
805 pub serve_inline: Option<bool>,
806}
807
808/// A single `[files.<name>]` configuration section.
809///
810/// File-upload routes are **not yet wired** into the binary; this type exists so
811/// the server can warn rather than silently ignore the section. All fields are
812/// optional to keep parsing tolerant.
813#[derive(Debug, Clone, Default, Serialize, Deserialize)]
814pub struct FileSectionConfig {
815 /// Named storage backend this upload route writes to.
816 #[serde(default)]
817 pub storage: Option<String>,
818
819 /// Maximum upload size (e.g. `"50MB"`).
820 #[serde(default)]
821 pub max_size: Option<String>,
822
823 /// URL path prefix override.
824 #[serde(default)]
825 pub path: Option<String>,
826}
827
828/// Multi-tenant runtime configuration (`[tenancy]`).
829#[derive(Debug, Clone, Default, Serialize, Deserialize)]
830pub struct TenancyServerConfig {
831 /// Per-tenant executor runtime settings (`[tenancy.runtime]`).
832 #[serde(default)]
833 pub runtime: TenancyRuntimeConfig,
834}
835
836/// Per-tenant executor runtime settings (`[tenancy.runtime]`).
837#[derive(Debug, Clone, Default, Serialize, Deserialize)]
838pub struct TenancyRuntimeConfig {
839 /// Mount the multi-tenant executor runtime (registry + admin tenant API +
840 /// `X-Tenant-ID` / JWT / Host dispatch). Defaults to `false`.
841 #[serde(default)]
842 pub enabled: bool,
843}
844
845impl Default for ServerConfig {
846 fn default() -> Self {
847 Self {
848 schema_path: default_schema_path(),
849 validate_sql_sources: false,
850 database_url: default_database_url(),
851 bind_addr: default_bind_addr(),
852 #[cfg(feature = "arrow")]
853 flight_bind_addr: defaults::default_flight_bind_addr(),
854 cors_enabled: true,
855 cors_origins: Vec::new(),
856 compression_enabled: false,
857 tracing_enabled: true,
858 otlp_endpoint: None,
859 otlp_export_timeout_secs: defaults::default_otlp_timeout_secs(),
860 tracing_service_name: defaults::default_service_name(),
861 apq_enabled: true,
862 cache_enabled: true,
863 graphql_path: default_graphql_path(),
864 health_path: default_health_path(),
865 readiness_path: default_readiness_path(),
866 introspection_path: default_introspection_path(),
867 metrics_path: default_metrics_path(),
868 metrics_json_path: default_metrics_json_path(),
869 playground_path: default_playground_path(),
870 playground_enabled: false, // Disabled by default for security
871 playground_tool: PlaygroundTool::default(),
872 subscription_path: default_subscription_path(),
873 subscriptions_enabled: true,
874 metrics_enabled: false, // Disabled by default for security
875 metrics_token: None,
876 admin_api_enabled: false, // Disabled by default for security
877 admin_token: None,
878 admin_readonly_token: None,
879 introspection_enabled: false, // Disabled by default for security
880 introspection_require_auth: true, // Require auth when enabled
881 metadata_require_auth: None, // Falls back to introspection_require_auth
882 schema_export_require_auth: None, // Falls back to introspection_require_auth
883 playground_require_auth: None, // Falls back to introspection_require_auth
884 subscription_require_auth: None, // Falls back to introspection_require_auth
885 design_api_require_auth: true, // Require auth for design endpoints
886 pool_min_size: default_pool_min_size(),
887 pool_max_size: default_pool_max_size(),
888 pool_timeout_secs: default_pool_timeout(),
889 auth: None, // No auth by default
890 auth_hs256: None, // No HS256 auth by default
891 hmac_secret_env: None, // No HMAC secret → unsigned idempotency token
892 tls: None, // TLS disabled by default
893 database_tls: None, /* Database TLS disabled
894 * by default */
895 require_json_content_type: true, // CSRF protection
896 max_request_body_bytes: default_max_request_body_bytes(), // 1 MB
897 max_header_count: default_max_header_count(), // 100 headers
898 max_header_bytes: default_max_header_bytes(), // 32 KiB
899 rate_limiting: None, // Rate limiting uses defaults
900 #[cfg(feature = "observers")]
901 observers: None, // Observers disabled by default
902 #[cfg(feature = "sources")]
903 sources: None, /* Source scheduler configured from the compiled
904 * schema by default */
905 pool_tuning: None, // Pool pressure monitoring disabled by default
906 admission_control: None, // Admission control disabled by default
907 security_contact: None, // No security.txt by default
908 validation: None, // Use compiled schema defaults
909 shutdown_timeout_secs: default_shutdown_timeout_secs(),
910 request_timeout_secs: None,
911 max_get_query_bytes: defaults::default_max_get_query_bytes(),
912 admin_auth_max_failures: defaults::default_admin_auth_max_failures(),
913 storage_token: None,
914 usage: None, // Usage persistence disabled by default
915 storage: HashMap::new(), // No storage backends wired by default
916 files: HashMap::new(), // No file-upload routes by default
917 #[cfg(feature = "inbound")]
918 webhooks: HashMap::new(), // No inbound webhook routes by default
919 #[cfg(feature = "inbound-email")]
920 mailbox: HashMap::new(), // No connected mailboxes by default
921 #[cfg(feature = "inbound-email")]
922 send: crate::inbound::email::SendSettings::default(),
923 tenancy: TenancyServerConfig::default(), // Multi-tenant runtime off by default
924 #[cfg(feature = "auth")]
925 identity: None, // Enriched-identity resolution off by default
926 }
927 }
928}