Filter Rules
Filter rules control which items are returned in a query. They are used in:
- Items API —
?filter[field][operator]=value - Permissions — restrict which rows a role can access
- Workflow assignments — determine which items match a workflow
Syntax
Filters follow this pattern:
filter[<field>][<operator>]=<value>For nested (relational) fields, use dot notation:
filter[author.status][_eq]=activeAs a JSON object (for POST bodies or permissions config):
{
"field": { "_operator": "value" }
}Shorthand Equality
When a filter field value is a primitive (string, number, boolean) instead of an operator object, it is treated as _eq:
{ "status": "published" }is equivalent to:
{ "status": { "_eq": "published" } }Comparison Operators
| Operator | Description | Example |
|---|---|---|
_eq | Equals | filter[status][_eq]=published |
_neq | Not equals | filter[status][_neq]=draft |
_lt | Less than | filter[views][_lt]=100 |
_lte | Less than or equal | filter[views][_lte]=100 |
_gt | Greater than | filter[views][_gt]=1000 |
_gte | Greater than or equal | filter[created_at][_gte]=2024-01-01 |
_in | In array | filter[status][_in]=draft,published |
_nin | Not in array | filter[status][_nin]=archived |
_null | Is null | filter[deleted_at][_null]=true |
_nnull | Is not null | filter[published_at][_nnull]=true |
_contains | Contains substring (case-sensitive) | filter[title][_contains]=hello |
_icontains | Contains substring (case-insensitive) | filter[title][_icontains]=hello |
_ncontains | Does not contain (case-sensitive) | filter[title][_ncontains]=spam |
_nicontains | Does not contain (case-insensitive) | filter[title][_nicontains]=spam |
_starts_with | Starts with (case-sensitive) | filter[slug][_starts_with]=2024- |
_istarts_with | Starts with (case-insensitive) | filter[slug][_istarts_with]=hello |
_nstarts_with | Does not start with (case-sensitive) | filter[slug][_nstarts_with]=draft- |
_nistarts_with | Does not start with (case-insensitive) | filter[slug][_nistarts_with]=draft- |
_ends_with | Ends with (case-sensitive) | filter[email][_ends_with]=@example.com |
_iends_with | Ends with (case-insensitive) | filter[email][_iends_with]=@example.com |
_nends_with | Does not end with (case-sensitive) | filter[email][_nends_with]=@spam.com |
_niends_with | Does not end with (case-insensitive) | filter[email][_niends_with]=@spam.com |
_between | Between two values (inclusive) | filter[price][_between]=10,100 |
_nbetween | Not between two values | filter[price][_nbetween]=10,100 |
_empty | Empty, null, or undefined | filter[bio][_empty]=true |
_nempty | Not empty, not null, and not undefined | filter[bio][_nempty]=true |
_regex | Matches regular expression | filter[slug][_regex]=^2024- |
_regex limitations: Uses PostgreSQL regex flavor (not JavaScript regex). In the database layer, it relies on the match operator which may require the pgcrypto extension on some Supabase plans. In the TypeScript evaluator (used for permission checks), it supports both wrapped /pattern/ syntax and raw pattern strings.
_icontains on arrays: When used in the TypeScript evaluator (permission checks), _icontains also scans array values, searching for the substring in any array element.
Logical Operators
Combine multiple conditions with _and or _or:
{
"_or": [
{ "status": { "_eq": "published" } },
{ "author": { "_eq": "$CURRENT_USER" } }
]
}{
"_and": [
{ "status": { "_eq": "published" } },
{ "views": { "_gte": 100 } }
]
}As query parameters (URL-encoded):
?filter[_and][0][status][_eq]=published&filter[_and][1][views][_gte]=100OR-condition operator support is limited. The following operators are supported in _or clauses: _eq, _neq, _gt, _gte, _lt, _lte, _in, _contains, _icontains, _starts_with, _istarts_with, _ends_with, _iends_with, _null, _nnull. Other operators (e.g., _ncontains, _between, _regex, _empty, _nin, and all negated case-insensitive variants) are silently ignored in _or clauses. Use _and with individual conditions or JSON notation if you need these operators.
Relational Filters
Filter by fields on related records using dot notation:
?filter[author.status][_eq]=active
?filter[tags.name][_in]=javascript,typescript
?filter[comments.approved][_eq]=trueSupported relation types: M2O (many-to-one), M2M (many-to-many), O2M (one-to-many).
Relational filters generate a subquery or JOIN on the related table. For M2M relations, the filter applies to any matching row in the junction table. The join type depends on whether _has is also specified — see below.
_has Operator
The _has operator controls whether parent rows with no matching children are included in results. It converts a left-outer join to an inner join, so only rows with at least one matching related record are returned.
Standalone usage — only return items that have at least one related role:
{ "roles": { "_has": true } }Combined with conditions — only return items that have at least one role with status=active:
{ "roles": { "_has": true, "status": { "_eq": "active" } } }Without _has, a left-outer join is used: parent rows are returned even if no related rows match the filter conditions.
Dynamic Variables
Use these special values in permission rules and API queries to create row-level access control:
| Variable | Resolves to |
|---|---|
$CURRENT_USER | The authenticated user’s ID |
$CURRENT_USER.field | A field value from the current user’s profile (e.g., $CURRENT_USER.team_id) |
$CURRENT_USER.relation.field | A nested relation value from the current user (M2M supported) |
$CURRENT_ROLE | The current user’s primary role ID |
$CURRENT_ROLES | Array of all role IDs the current user holds |
$CURRENT_POLICIES | Array of all policy IDs the current user holds |
$NOW | Current timestamp |
$CURRENT_RESOURCE_URI | The resource URI scope of the current request |
Example — let users only read their own records:
{ "user_id": { "_eq": "$CURRENT_USER" } }Example — restrict to items belonging to the current user’s team:
{ "team_id": { "_eq": "$CURRENT_USER.team_id" } }Array Values
For _in, _nin, _between, and _nbetween, pass a comma-separated string or a JSON array:
?filter[status][_in]=draft,published{ "status": { "_in": ["draft", "published"] } }