Skip to main content
A Row-Level Filter limits which records a role can see — not just which tables. You attach it to a role’s permission on a table, and every read, list, count, update, and delete for that role is automatically scoped to the matching rows. Users never see (or touch) records outside their filter, and they don’t have to add anything to their queries — it’s enforced server-side on every request. You define it as a small JSON object in the Custom Filter editor of the Role-Based Access permission matrix.
A row-level filter narrows what a role can access; it never grants access. A role still needs the base Read / Update / Delete permission on the table. Without the base permission, the filter does nothing.

Opening the Custom Filter

1

Go to Role-Based Access

Backend Console → App Services → Role-Based Access, then click the role you want to scope.
2

Open the Filter for a table

In the permission matrix, find the table’s row and open its Custom Filter. A small JSON editor appears.
3

Paste your filter and Save

Paste the JSON (see the shapes below) and click Save. It takes effect on the next request for that role.

The two modes: applyAs

The Custom Filter has an applyAs field that decides how the filter is used: To scope rows, set "applyAs": "row_filter". If you leave applyAs out, the filter behaves as a gate (the existing default) — so adding row-level scoping is always an explicit choice and never changes existing filters.

Filter shape

A row-level filter is a JSON object with four keys:

Field rules

Each entry in rules is a single condition on one column:

Operators

in and not_in use values (a list). Every other operator uses value (a single value).

Dynamic values (the signed-in user)

Instead of a fixed value, a rule can reference the authenticated user so the same filter scopes each user to their own records:
Use the token that matches how your column stores the user. If your owner_id column holds emails, $user.email is the natural choice; if it stores the user identifier, use $user.id. In most projects the user’s identifier is their email, so the two are often interchangeable — pick the one whose value matches your column so rows actually match.

Combining rules: logic

logic controls how multiple rules combine:
  • "AND" — a row must match every rule.
  • "OR" — a row must match at least one rule.
The example above scopes a user to their own, non-archived orders.

Multiple roles combine as a union

If a user holds several roles and more than one has a row-level filter on the same table, the results are the union (OR) of all their filters. A user is never more restricted for having an extra role — each role’s filter can only add rows they’re allowed to see. Example: role stylist filters appointments to status = PENDING, role receptionist filters to status = IN_PROGRESS. A user with both roles sees appointments that are PENDING or IN_PROGRESS.

Where the filter applies

Once attached to a role’s table permission, a row-level filter is enforced on:

Writes are scoped too

The filter isn’t only for reads — it also protects updates and deletes, so a role can never modify or remove a record it isn’t allowed to see. This holds for single-record and bulk operations, and you don’t change the mutation at all — the scope is applied automatically. Take a role scoped to its own orders:
With that filter, the same mutations a user already runs are silently limited to their own rows:
The write scope is enforced at the database level, so it holds even under concurrent activity: a row that leaves the filter between the request starting and the write committing is still not modified. The same guarantee applies on both the GraphQL and REST APIs.

Examples

Users see only their records

Only active, recent records

Domain-scoped by email

Everything except archived

Rules & limits

A row-level filter only accepts "type": "field" rules. Free-form SQL and expression rules are intentionally not allowed — you describe what to match with columns, operators, and values, and the platform builds a safe, parameterized query for you.
  • Values are always bound safely. Nothing you type in value / values is ever executed as code, so filters can’t be used for injection.
  • Match your value types exactly:
    • Enums / status columns are case-sensitive. If the stored value is PENDING, filter on "PENDING", not "pending".
    • Numbers go without quotes ("value": 100, not "value": "100"), otherwise they’re treated as text.
    • Booleans use true / false (no quotes).
  • in / not_in need values (a list); every other operator needs value.
  • Unknown column or operator ⇒ the filter is rejected safely. If a rule references a column that doesn’t exist or an operator that isn’t supported, the role sees no rows for that table (fail-safe) rather than accidentally exposing everything. Double-check field and operator spelling if a filter returns nothing unexpectedly.

Troubleshooting

  • Confirm the filter is saved on the correct role, the correct table, and that applyAs is exactly "row_filter".
  • You may be querying as a project owner / administrator, which is not scoped by design. Test with a user that only holds the filtered role.
  • Check the base Read permission is enabled for that table (the filter narrows Read; it doesn’t replace it).
  • Value mismatch is the most common cause — usually enum casing ("PENDING" vs "pending") or a number sent as text.
  • A $user.id / $user.email token that doesn’t match how the column stores the user returns nothing. Use the token whose value matches your column.
  • A misspelled field or operator makes the filter fail safe to no rows.
That’s usually correct, not a bug: greater_than: 10.5 returns nothing if every row is 10.2. Flip the threshold (e.g. greater_than: 10) to confirm the filter is applied — matching rows should reappear.
Changes to a role’s filter take effect within a short window (typically under a minute) as the new rules propagate. If a change doesn’t seem to apply immediately, wait a moment and retry.