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 rows | An application policy |
| Inspect or adjust a query and its result | An interceptor |
| Inspect or adjust a write before or after it happens | A 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().
