Skip to content
Concepts

Hooks, interceptors, and policies

An application may want every user directory query to hide certain accounts, or every new account to follow the same naming rule. Put that shared behavior around the generated client, so individual handlers can keep doing ordinary reads and writes.

There are three places to attach it:

You want to…Use…
Add an application-specific restriction on an operation or its rowsAn application policy
Inspect or adjust a query and its resultAn interceptor
Inspect or adjust a write before or after it happensA mutation hook

Use tenancy to keep customers separate and permissions in RSL for resource ownership, sharing, and inherited access. The Rust APIs on this page add behavior around those rules. You do not need a custom policy to turn either feature on. If you are choosing between them, start with tenancy and permissions.

Start with a before-write hook

Suppose a signup flow accepts Maya, but this example wants to store the full display name. A hook can change that value before the create runs:

use rent::policy::hook::{HookChain, HookStage, MutationValue, Operation};
use rent::runtime::{MutationContext, MutationOutcome};

let hooks = HookChain::<MutationOutcome, MutationContext>::default()
    .append(|event| {
        let is_maya = matches!(
            event.mutation.field("name"),
            Some(MutationValue::String(name)) if name == "Maya"
        );
        if event.stage == HookStage::Before
            && event.context.entity == "User"
            && event.mutation.operation == Operation::Create
            && is_maya
        {
            event.mutation.set("name", "Maya Chen");
        }
        Ok(())
    });

let client = Client::new(pool).with_hooks(hooks);
let maya = client.create_user().name("Maya").save().await?;

The saved user has the name Maya Chen. Application code still uses the normal create builder. The middleware tutorial adds a policy and query interceptor to the same pattern.

Configure behavior once

Application policies can allow, deny, or add conditions before an operation reaches SQL. For example, an application might reject writes while an account is under review. Allowing an operation in a custom policy does not override a required tenant boundary or a denied RSL permission. Interceptors can inspect or transform query specifications and outcomes. Mutation hooks wrap creates, updates, deletes, relationship changes, and permission grants or revocations.

Transaction clients inherit the context, policies, interceptors, hooks, and field codecs. Configure them on the root client, then pass scoped clients into request code. See root and scoped clients.

Keep access restrictions in place

Policies can narrow a query or mutation's selection, but cannot widen the caller's filters. Identity, optimistic-version, and relationship-key conditions remain required even when a policy replaces its predicate. A policy that hides archived records therefore still respects .id_eq(id) and the version supplied to an optimistic update.

An upsert that conflicts with an inaccessible row returns a policy denial without exposing that row's unique values or database constraint details. The same rule applies when another unique key conflicts.

Relationship filters and traversals prepare policies for every participating entity. For example, user().has_posts() considers only posts visible under the post policy, while user().query_posts() also respects the source user policy. Denying an entity prevents accessing it through a relationship as well as directly. Query inspection applies these same preparation rules before showing the SQL.

Handle write failures

After-hook failure and cancellation roll back the write.

Inspect permission assignments

The permission grant API exposes Operation::Grant and Operation::Revoke. Their mutation.assignment() metadata contains complete resource and subject identities, the named rule, an optional userset rule, and optional expiry. It is separate from model-field changes: granting a post's viewer permission does not update the post's fields. Hooks may inspect or reject the assignment. Changing its metadata or adding model-field mutations is rejected. Policies can narrow the resource predicate, while both endpoint visibility and the resource's manage_grants permission remain required.

Keep raw SQL explicit

Raw expressions are trusted application code. filter_raw, Predicate::from_raw, and expression-based ordering preserve the outer query's policy processing, but Rent does not scope SQL subqueries supplied inside them. Their author must authorize every referenced table and bind input values. Prefer generated relationship filters when querying related entities. Standalone raw statements require system().