Skip to main content

RuntimeConfig

Struct RuntimeConfig 

Source
pub struct RuntimeConfig {
Show 21 fields pub cache_query_plans: bool, pub max_page_size: Option<u32>, pub enable_tracing: bool, pub field_filter: Option<FieldFilter>, pub rls_policy: Option<Arc<dyn RLSPolicy>>, pub field_authorizer: Option<Arc<dyn FieldAuthorizer>>, pub authorizer: Option<Arc<dyn Authorizer>>, pub query_timeout_ms: u64, pub jsonb_optimization: JsonbOptimizationOptions, pub query_validation: Option<QueryValidatorConfig>, pub max_operation_cost: Option<u64>, pub max_response_bytes: Option<u64>, pub audit_mutations: bool, pub changelog_enabled: bool, pub dry_run_mutations: bool, pub mutation_error_shape_check: MutationErrorShapeCheck, pub cascade_limits: CascadeLimits, pub before_mutation_gate: Option<Arc<dyn BeforeMutationGate>>, pub query_function_resolver: Option<Arc<dyn QueryFunctionResolver>>, pub after_mutation_observer: Option<Arc<dyn AfterMutationObserver>>, pub root_error_renderer: Option<RootErrorRenderer>,
}
Expand description

Runtime configuration for the FraiseQL query executor.

Controls safety limits, security policies, and performance tuning. All settings have production-safe defaults and can be overridden via the builder-style methods.

§Defaults

FieldDefaultNotes
cache_query_planstrueCaches parsed query plans for repeated queries
query_validationNoneDerived from compiled [validation] limits (#379)
enable_tracingfalseEmit OpenTelemetry spans for each query
query_timeout_ms30 000Hard limit; 0 disables the timeout
field_filterNoneNo field-level access control
rls_policyNoneNo row-level security
authorizerNoneNo operation-level authorization
before_mutation_gateNoneNo before:mutation enforcement
query_function_resolverNoneA function-backed root field refuses, by name
after_mutation_observerNoneNo one is told a write committed
root_error_rendererNoneA failing root’s errors entry carries the error’s text

§Example

use fraiseql_core::runtime::RuntimeConfig;
use fraiseql_core::security::FieldFilterConfig;

let config = RuntimeConfig {
    enable_tracing: true,
    query_timeout_ms: 5_000,
    ..RuntimeConfig::default()
}
.with_field_filter(
    FieldFilterConfig::new()
        .protect_field("User", "salary")
        .protect_field("User", "ssn"),
);

Clone is derived (the trait-object fields are behind Arc) so a rebuild — a hot-reload, a per-tenant executor — can start from the live config and re-apply only what the new compiled schema owns, via with_compiled_schema.

Fields§

§cache_query_plans: bool

Enable query plan caching.

§max_page_size: Option<u32>

Maximum number of rows a top-level first/last/limit argument may request, guarding against unbounded-pagination denial of service (#421): the top-level row count is the one knob that sizes the database result set and the serialized response. A request exceeding this is rejected with a crate::FraiseQLError::Validation. None disables the ceiling. Default Some(1000).

§enable_tracing: bool

Enable performance tracing.

§field_filter: Option<FieldFilter>

Optional field filter for access control. When set, validates that users have required scopes to access fields.

§rls_policy: Option<Arc<dyn RLSPolicy>>

Optional row-level security (RLS) policy. When set, evaluates access rules based on SecurityContext to determine what rows a user can access (e.g., tenant isolation, owner-based access).

§field_authorizer: Option<Arc<dyn FieldAuthorizer>>

Optional dynamic field-level authorizer.

When set, fields marked policy-gated in the compiled schema (FieldDefinition::authorize) are passed to this authorizer per row, which returns an allow/deny decision based on the principal, the parent row, and the field arguments. Composes as a logical AND with the static requires_scope gate and is fail-closed (any error denies). See FieldAuthorizer.

§authorizer: Option<Arc<dyn Authorizer>>

Optional dynamic operation-level authorizer.

When set, every operation (query, mutation, subscription) is passed to this authorizer before dispatch, which returns an allow/deny decision based on the principal (or None when anonymous), the operation kind and name, and the request input. Composes as a logical AND with the static requires_role gate and is fail-closed (any error or raise denies with HTTP 403 / FORBIDDEN). See Authorizer.

§query_timeout_ms: u64

Query timeout in milliseconds (0 = no timeout).

§jsonb_optimization: JsonbOptimizationOptions

JSONB field optimization strategy options

§query_validation: Option<QueryValidatorConfig>

Optional query validation config.

When Some, QueryValidator runs at the start of every Executor::execute() call, before any parsing or SQL dispatch. This provides DoS protection for direct fraiseql-core embedders that do not route through fraiseql-server (which already runs RequestValidator at the HTTP layer). Enforces: query size, depth, complexity, and alias count (alias amplification protection).

When None (the default), the executor derives the gate from the compiled schema’s declared [validation] limits at construction (#379), so an operator’s declared bound binds on every transport that reaches the executor. An embedder that must disable validation despite declared schema limits can install an explicit all-usize::MAX config.

§max_operation_cost: Option<u64>

Hard ceiling on a single operation’s estimated cost (#379).

Schema-derived — recomputed from the compiled [security.cost_budget] per_request_max on every with_compiled_schema, never set by the caller. Enforced in the executor’s dispatch alongside GATE-1, so the operator’s declared ceiling binds on every transport that executes a GraphQL document — not only on /graphql.

§max_response_bytes: Option<u64>

Hard ceiling on the bytes a single read may deliver.

Schema-derived — recomputed from the compiled [validation] max_response_bytes on every with_compiled_schema, never set by the caller.

Unlike max_operation_cost this is not scored before the database runs: what a read weighs is not recoverable from the request. It is resolved at the read chokepoint and charged on the rows that come back, so it binds on every transport that returns rows — documents, REST reads and exports, and both gRPC arms — and not only on the ones that carry a document to score. See ResponseBudget.

§audit_mutations: bool

Emit structured tracing events for every successfully-executed mutation.

When true, a tracing::info! event with target "fraiseql::mutation_audit" is emitted at the end of every successful execute_mutation_query_with_security() call. The event carries fields: mutation_name, entity_type, operation, tenant_id.

Zero-cost when disabled: the guard if !self.config.audit_mutations { return } short-circuits before any string formatting or allocation occurs.

Set to true when audit_logging_enabled = true in the compiled schema’s [security.enterprise] section (threaded through Server::new() at startup).

§changelog_enabled: bool

Global switch for the Change-Spine change-log outbox write (default true).

When true, every successful state-changing mutation writes one core.tb_entity_change_log row in-transaction (the framework owns the write). Set false to disable the outbox globally — e.g. for an application that does not consume the Change Spine — so no mutation pays the write. The per-mutation MutationDefinition.changelog flag composes as a logical AND on top of this: a row is written only when the global switch is on and the mutation is not individually opted out.

Sourced from [changelog] enabled in fraiseql.toml, overridable at runtime by FRAISEQL_CHANGELOG_ENABLED.

§dry_run_mutations: bool

Validate-bind-without-commit mode for mutations (default false).

When true, every state-changing mutation is executed inside a database transaction that is rolled back instead of committed: the function binds and runs (so constraints, triggers, and the mutation_response shape are all validated) but no writes persist and no change-log row is emitted. Powers the fraiseql query --dry-run CLI smoke check and the doctor --runtime mutation probes.

A dry run is the same write in WriteMode::DryRun (ruling AA 6): the same transaction, session variables, outbox row and gate, ending in a rollback, and nothing that follows a commit follows it. Queries are unaffected (they never commit).

§mutation_error_shape_check: MutationErrorShapeCheck

Check a failed mutation’s error_detail.errors[] (#1425): off by default, see MutationErrorShapeCheck.

Sourced from the server’s mutation_error_shape_check key.

§cascade_limits: CascadeLimits

Response-size guards for the typed cascade surface (graphql-cascade 16_security). A cascade mutation returning more affected entities than CascadeLimits::max_updated_entities is truncated with truncated metadata; one exceeding CascadeLimits::max_response_size_mb is rejected. Same DoS-guard family as max_page_size.

§before_mutation_gate: Option<Arc<dyn BeforeMutationGate>>

Optional before:mutation enforcement point (#1327).

When set, every mutation the engine is about to execute is passed to this gate first — per root field, in document order, with the arguments the write will bind from (request variables merged with inline literals). The gate may let the write proceed, rewrite its arguments, or refuse it. Fail-closed: any error refuses the write.

Enforced at execute_mutation_impl, the single point every mutation entry path converges on, so a transport cannot reach a write without passing it. See BeforeMutationGate.

§query_function_resolver: Option<Arc<dyn QueryFunctionResolver>>

Optional resolver for function-backed root query fields (#1329).

When a compiled query declares function = "<name>", the engine asks this resolver for the field’s data instead of reading a relation, then projects, field-filters and caches the answer exactly as it would a SQL-backed one.

None means a function-backed field is refused by name rather than answered — an embedder who loaded a schema declaring one and wired no resolver has a field that cannot work, and the refusal says so. The stock binary wires one whenever the compiled schema carries a functions section, and #1326 already refuses to boot a build that cannot serve one.

See QueryFunctionResolver.

§after_mutation_observer: Option<Arc<dyn AfterMutationObserver>>

Optional observer told of every committed mutation (#1340, #1440).

Called from execute_mutation_impl once per committed success, with the type the write produced and the full entity, so every transport fires the same after:mutation work and a failed write never does. See AfterMutationObserver.

§root_error_renderer: Option<RootErrorRenderer>

Optional renderer for the errors entry of a failing root that the engine reports inside a response rather than as an Err.

A mutation with more than one root reports a failing root as null plus an errors entry, and the response is still Ok, so the embedder’s own error handling (sanitization above all) never sees that error. The renderer gives the entry the treatment a failed request gets; the engine adds its path. None renders {"message": <the error's text>}.

Implementations§

Source§

impl RuntimeConfig

Source

pub fn with_field_filter(self, config: FieldFilterConfig) -> Self

Create a new runtime config with a field filter.

§Example
use fraiseql_core::runtime::RuntimeConfig;
use fraiseql_core::security::FieldFilterConfig;

let config = RuntimeConfig::default()
    .with_field_filter(
        FieldFilterConfig::new()
            .protect_field("User", "salary")
            .protect_field("User", "ssn")
    );
Source

pub fn with_rls_policy(self, policy: Arc<dyn RLSPolicy>) -> Self

Configure row-level security (RLS) policy for access control.

When set, the executor will evaluate the RLS policy before executing queries, applying WHERE clause filters based on the user’s SecurityContext.

§Example
use fraiseql_core::runtime::RuntimeConfig;
use fraiseql_core::security::DefaultRLSPolicy;
use std::sync::Arc;

let config = RuntimeConfig::default()
    .with_rls_policy(Arc::new(DefaultRLSPolicy::new()));
Source

pub fn with_field_authorizer(self, authorizer: Arc<dyn FieldAuthorizer>) -> Self

Configure a dynamic field-level authorizer.

When set, fields marked policy-gated in the compiled schema (FieldDefinition::authorize) are evaluated per row by this authorizer. The decision composes as a logical AND with the static requires_scope gate and is fail-closed (any error or raise denies with HTTP 403 / FORBIDDEN). Parallel to with_rls_policy.

§Example
use fraiseql_core::runtime::RuntimeConfig;
use fraiseql_core::security::{
    FieldAuthorizer, FieldAuthzRequest, FieldAuthzDecision,
};
use fraiseql_core::error::Result;
use std::sync::Arc;

struct AllowAll;
impl FieldAuthorizer for AllowAll {
    fn authorize_field(&self, _req: &FieldAuthzRequest<'_>) -> Result<FieldAuthzDecision> {
        Ok(FieldAuthzDecision::Allow)
    }
}

let config = RuntimeConfig::default().with_field_authorizer(Arc::new(AllowAll));
Source

pub const fn with_cascade_limits(self, limits: CascadeLimits) -> Self

Override the cascade response-size limits (graphql-cascade 16_security).

Source

pub fn with_authorizer(self, authorizer: Arc<dyn Authorizer>) -> Self

Configure a dynamic operation-level authorizer.

When set, every operation (query, mutation, subscription) is passed to this authorizer before dispatch. The decision composes as a logical AND with the static requires_role gate and is fail-closed (any error or raise denies with HTTP 403 / FORBIDDEN). Parallel to with_field_authorizer and with_rls_policy.

§Example
use fraiseql_core::runtime::RuntimeConfig;
use fraiseql_core::security::{Authorizer, AuthzRequest, AuthzDecision};
use fraiseql_core::error::Result;
use std::sync::Arc;

struct AllowAll;
impl Authorizer for AllowAll {
    fn authorize(&self, _req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
        Ok(AuthzDecision::Allow)
    }
}

let config = RuntimeConfig::default().with_authorizer(Arc::new(AllowAll));
Source

pub fn with_before_mutation_gate( self, gate: Arc<dyn BeforeMutationGate>, ) -> Self

Configure the before:mutation enforcement point (#1327).

When set, every mutation the engine executes is adjudicated by this gate first — per root field, in document order, with the arguments the write will bind from. It may proceed, rewrite the arguments, or refuse. Installed at execute_mutation_impl, the chokepoint every transport converges on, so no route can execute a write without running it. Fail-closed: any error refuses the write.

§Example
use fraiseql_core::runtime::RuntimeConfig;
use fraiseql_core::security::{
    BeforeMutationGate, BeforeMutationOutcome, BeforeMutationRequest,
};
use fraiseql_core::error::Result;
use std::sync::Arc;

struct AllowAll;

#[async_trait::async_trait]
impl BeforeMutationGate for AllowAll {
    async fn before_mutation(
        &self,
        _req: &BeforeMutationRequest<'_>,
    ) -> Result<BeforeMutationOutcome> {
        Ok(BeforeMutationOutcome::Proceed)
    }
}

let config = RuntimeConfig::default().with_before_mutation_gate(Arc::new(AllowAll));
Source

pub fn with_query_function_resolver( self, resolver: Arc<dyn QueryFunctionResolver>, ) -> Self

Register the resolver for function-backed root query fields (#1329).

Parallel to with_before_mutation_gate. Without one, a query declaring function = "<name>" is refused by name rather than answered.

Source

pub fn with_after_mutation_observer( self, observer: Arc<dyn AfterMutationObserver>, ) -> Self

Register the observer told of every committed mutation (#1340, #1440).

Parallel to with_before_mutation_gate: one slot on the config, consulted at the write chokepoint every transport converges on.

Source

pub fn with_root_error_renderer(self, renderer: RootErrorRenderer) -> Self

Register the renderer for a failing root’s in-response errors entry.

Parallel to with_before_mutation_gate. See root_error_renderer.

Source

pub fn from_compiled_schema(schema: &CompiledSchema) -> Result<Self, String>

Build a RuntimeConfig from a compiled schema, applying every schema-derived runtime setting that an executor must honor.

This is the single seam every server entry point routes through so the config can never drift by constructor (H16): Server::new, with_relay_pagination, and with_flight_service previously built the executor with RuntimeConfig::default, silently dropping the compiled audit-logging flag, the #421 page-size ceiling, and the change-log toggle, and skipping the schema-format-version check.

Applied in order:

  1. Schema-format-version validation — a legacy schema (no version) warns; an incompatible future version is rejected. Coupling the check into the constructor means a caller cannot obtain a config while skipping the validation.
  2. Audit logging — audit_mutations from the compiled [security.enterprise] audit_logging_enabled.
  3. Page-size ceiling (#421) — FRAISEQL_MAX_PAGE_SIZE overrides the compiled [validation] max_page_size, which overrides the default.
  4. Change-log outbox toggle — FRAISEQL_CHANGELOG_ENABLED overrides the compiled [changelog] write_enabled (default true).
§Errors

Returns the validation message when the compiled schema was produced by a different fraiseql build (#1304).

Source

pub fn with_compiled_schema( self, schema: &CompiledSchema, ) -> Result<Self, String>

Re-apply every schema-derived setting on top of an existing config.

from_compiled_schema is this method over Self::default. The split exists for rebuilds: a hot-reload must re-derive the settings the new compiled schema owns while preserving the ones the embedder installed programmatically (authorizers, RLS policy, field filter, validator). Reconstructing from default() on reload is what silently reverted audit logging, the page-size ceiling and the change-log toggle on every SIGUSR1 (#750).

§Errors

Returns the validation message when the compiled schema was produced by a different fraiseql build (#1304).

Trait Implementations§

Source§

impl Clone for RuntimeConfig

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for RuntimeConfig

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for RuntimeConfig

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more