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