Drizzle Castor enforces security at the data access layer itself, not in service functions scattered across your application. Every call to a repository method passes through the Unified RBAC middleware, which evaluates the active profile against registered policies, trims or rejects unauthorized fields, and either allows or halts execution — all before a single SQL statement is constructed or sent to the database.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/fajarnugraha37/drizzle-castor/llms.txt
Use this file to discover all available pages before exploring further.
How the RBAC middleware fits in the pipeline
The full lifecycle of a query looks like this:createUnifiedRbacMiddleware is composed into the middleware stack automatically when you call builder.build(). It runs first, intercepting every action regardless of which repository method triggered it. The middleware reads the active profile from the execution context, resolves the matching policy, and applies access checks before calling next() to hand off to the query translator.
The RBAC middleware is not optional — it is always present in the middleware pipeline. Tables without a registered policy are handled according to the configured execution mode (strict or lenient).
Strict vs lenient mode
When you create a schema builder withcreateSchemaBuilder, you choose an execution mode as the third argument:
- Lenient (default)
- Strict
If a table has no policy registered at all, all actions are permitted. Unspecified rules default to allowing access. This is useful during development when you want to start wiring up policies incrementally without immediately locking down every table.
The five control dimensions
EveryUnifiedPolicyConfig object can specify up to five dimensions of access control. Each dimension controls a different clause of the resulting SQL query.
allowedActions — which operations are permitted
allowedActions — which operations are permitted
Controls which database operations the profile can execute. The available action strings map directly to repository methods:
Set this to
| Action string | Repository methods |
|---|---|
"create" | createOne, createMany |
"read" | searchOne, searchMany, searchPage |
"update" | updateOne, updateMany |
"softDelete" | softDeleteOne, softDeleteMany |
"restore" | restoreOne |
"hardDelete" | hardDeleteOne, hardDeleteMany |
"*" to grant unrestricted access to all operations. If the active profile attempts an action not in the array, the middleware throws AccessDeniedError immediately.allowedProjections — what data can be returned (SELECT)
allowedProjections — what data can be returned (SELECT)
Restricts which fields appear in the
SELECT clause. If a caller requests projection: ["name", "email", "passwordHash"] but only "name" and "email" are in allowedProjections, the query executes as SELECT name, email FROM ... — the unauthorized field is silently dropped.allowedFilters — which fields can appear in WHERE clauses
allowedFilters — which fields can appear in WHERE clauses
Controls which fields a profile may filter against. The engine recursively walks the
$and/$or/$not filter tree and discards any leaf node that references a disallowed field. The rest of the filter continues to execute normally.allowedSets — which columns can be written (INSERT / UPDATE)
allowedSets — which columns can be written (INSERT / UPDATE)
Prevents writing to restricted columns during
create and update operations. If the payload includes { name: "John", role: "admin" } but "role" is not in allowedSets, only the name update is passed to the database.allowedSorts — which fields can appear in ORDER BY
allowedSorts — which fields can appear in ORDER BY
Limits which columns a profile may sort by. Unauthorized sort keys are stripped from the
order object before it reaches the AST translator.Intelligent Data Trimming
Rather than throwing a hard error the moment an unauthorized field appears in a projection, filter, or sort clause, Drizzle Castor applies Intelligent Data Trimming by default: unauthorized fields are silently removed and a warning is recorded inctx.state.warnings. A security event is also emitted on the event bus so you can route it to your audit trail.
- Projections: unauthorized fields are dropped; the query still executes with the permitted subset.
- Filters: the specific filter node for the unauthorized field is discarded; sibling conditions inside
$and/$orare preserved. - Sets (create/update): unauthorized keys are removed from the payload before the mutation reaches the database.
- Sorts: unauthorized sort keys are deleted from the
orderobject.
If trimming leaves a clause completely empty (for example, every field in the projection was unauthorized), the middleware throws
AccessDeniedError rather than continuing with an empty clause. Partial access is allowed; zero access is denied.builder.withThrowError(true). In that mode, any unauthorized field immediately raises AccessDeniedError instead of being silently removed.
How multiple profiles are merged
You can pass a single profile string or an array of profiles to any repository method:- Actions: the union of all allowed action lists. If any profile has
"*", the result is"*". - Field dimensions (projections, filters, sets, sorts): arrays are deduplicated and merged. If any profile has
"*"for a dimension, the merged result is"*".
"public" and "editor" profiles gains the combined permissions of both, without either profile needing to enumerate the other’s fields.
Next steps
Defining policies
Learn how to define table-specific policies, dynamic async callbacks, and global fallback policies.
Working with profiles
Understand how to declare profiles with TypeScript safety and pass them to repository methods.