Access Rules & Filter Expressions
Enforce row-level security, role-based access, and field constraints with HCL-like rule expressions.
Every dynamic collection in Moul is guarded by five row-level access control rules evaluated dynamically on incoming client requests:
listRule— Restricts which records are returned in collection list queries.viewRule— Controls access to view a single record by ID.createRule— Validates incoming fields and permissions before record insertion.updateRule— Validates current record fields and incoming values before update.deleteRule— Validates permissions before record deletion.
Rule Access States
| Value | Access State |
|---|---|
null | Admin Only: Only requests carrying a valid master X-Admin-Key header can perform this operation. Public and user requests receive HTTP 403 Forbidden. |
"" (Empty string) | Public: Anyone (including unauthenticated guests) can perform this operation. |
"expression..." | Conditional: The operation is allowed only if the expression evaluates to true against the request context and record state. |
Expression Syntax Reference
Context Variables
@request.auth.id: Authenticated user's unique record ID.@request.auth.email: Authenticated user's email.@request.auth.*: Any custom field present on the authenticated user's record.@request.body.fieldName: Value submitted in incoming JSON request body.@request.headers.header_name: Incoming HTTP request header value.@request.query.paramName: URL query parameter value.@request.method: HTTP request method (GET,POST,PATCH,DELETE).
Operators
| Operator | Meaning | Example |
|---|---|---|
= | Equal to | status = 'published' |
!= | Not equal to | @request.auth.id != '' |
>, >= | Greater than / Greater than or equal | views >= 100 |
<, <= | Less than / Less than or equal | age < 18 |
~ | Contains / LIKE substring match | title ~ 'tutorial' |
!~ | Does not contain substring | email !~ '@spam.com' |
&&, || | Logical AND / Logical OR | published = true || authorId = @request.auth.id |
?= | Array modifier (contains element) | collaboratorIds.id ?= @request.auth.id |
Field Modifiers
:lower: Converts string to lowercase for case-insensitive comparison (e.g.email:lower = @request.body.email:lower).:length: Returns length of string or array (e.g.content:length > 20).:isset: Checks if a field was provided in request body (e.g.@request.body.password:isset = true).:changed: Checks if field value is being modified in an update request (e.g.@request.body.role:changed = false).:each: Evaluates condition across all elements of an array.
Helper Functions
geoDistance(lon1, lat1, lon2, lat2): Calculates distance in kilometers between two geographic coordinates.strftime(format, timestamp): Evaluates SQLite datetime string formatting.
Cross-Collection Join Queries
Access rules can query other collections dynamically using @collection syntax:
@collection.userRoles.userId = @request.auth.id && @collection.userRoles.role = 'admin'Common Rule Recipes
1. Public Read, Authenticated Write
listRule: ""
viewRule: ""
createRule: "@request.auth.id != ''"
updateRule: "authorId = @request.auth.id"
deleteRule: "authorId = @request.auth.id"2. Multi-Tenant Organization Isolation
listRule: "orgId = @request.auth.orgId"
viewRule: "orgId = @request.auth.orgId"
createRule: "@request.body.orgId = @request.auth.orgId"
updateRule: "orgId = @request.auth.orgId"
deleteRule: "orgId = @request.auth.orgId && @request.auth.role = 'org_admin'"3. Role-Based Access Control: users Auth Collection
Configure the users auth collection with custom fields:
role: Typeselect, Options:["admin", "editor", "public"], Required:true- Optional profile fields:
name(text),avatar(urlorfile),bio(text)
Access rules prevent privilege escalation while enabling self-service profile management and administrative oversight:
listRule: "id = @request.auth.id || @collection.users.id = @request.auth.id && (@collection.users.role = 'editor' || @collection.users.role = 'admin')"
viewRule: "id = @request.auth.id || @collection.users.id = @request.auth.id && (@collection.users.role = 'editor' || @collection.users.role = 'admin')"
createRule: "@request.body.role:isset = false || @request.body.role = 'public' || (@collection.users.id = @request.auth.id && @collection.users.role = 'admin')"
updateRule: "(@collection.users.id = @request.auth.id && @collection.users.role = 'admin') || (id = @request.auth.id && @request.body.role:changed = false)"
deleteRule: "(@collection.users.id = @request.auth.id && @collection.users.role = 'admin') || id = @request.auth.id"createRule: Allows open registration with thepublicrole (or omitting the field to accept default), while preventing unauthorized users from assigning themselveseditororadmin. Only existing admins can provision elevated roles directly.updateRule: Users can update their own profile fields, but cannot modify their assignedrole(@request.body.role:changed = false). Only anadmincan promote or demote user roles.deleteRule: Users can self-delete their own account (id = @request.auth.id), while anadmincan delete any user record.
4. Enforcing Role-Level Access on Downstream Collections (e.g. posts)
Downstream business collections query the users auth collection using @collection relational lookups to enforce role-level privileges (public | editor | admin):
authorId: Typerelationtargetingusers(1:1)status: Typeselect, Options:["draft", "published", "archived"]
listRule: "status = 'published' || (@collection.users.id = @request.auth.id && (@collection.users.role = 'editor' || @collection.users.role = 'admin'))"
viewRule: "status = 'published' || (@collection.users.id = @request.auth.id && (@collection.users.role = 'editor' || @collection.users.role = 'admin'))"
createRule: "@collection.users.id = @request.auth.id && (@collection.users.role = 'editor' || @collection.users.role = 'admin')"
updateRule: "(@collection.users.id = @request.auth.id && @collection.users.role = 'admin') || (authorId = @request.auth.id && @collection.users.id = @request.auth.id && @collection.users.role = 'editor')"
deleteRule: "@collection.users.id = @request.auth.id && @collection.users.role = 'admin'"- Public: Anyone can list and view records where
status = 'published'. - Editor: Can view unpublished drafts, create new posts, and edit posts where they are the designated author (
authorId = @request.auth.id). - Admin: Has full access to view, create, edit, publish, and delete all records.
Testing Rules via CLI Sandbox
You can test and benchmark rules locally without executing live HTTP requests:
moul test-rule \
--rule="authorId = @request.auth.id && views > 100" \
--record='{"authorId": "usr_001", "views": 250}' \
--auth='{"id": "usr_001"}'