Groups and row-level security
Put users in groups and grant records to those groups, so row-level security scopes reads and writes without impersonation endpoints.
Last updated
Row-level security (RLS) decides which individual records each user can see and change. Turn it on for an entity and every read, update and delete through the REST API is filtered to the records that user has been granted: by name, or through a group they belong to. Your app calls the same endpoints with the user's token. You don't write per-user queries, filter results on the client, or add impersonation endpoints.
Anythink has three layers of access control, and they stack:
| Layer | Question it answers | Configure it in |
|---|---|---|
| Role permissions | Can this user read, create, update or delete orders at all? |
Roles and permissions |
| Field-level permissions | Which fields of an orders record can they see? |
Roles and permissions |
| Row-level security | Which orders records can they see and change? |
This page |
Use roles when everyone with a job title sees the same data. Use field permissions to hide columns such as cost price or internal notes. Use row-level security when two users with the same role should see different records, such as a customer's own orders, a coach's athletes, or a sales team's accounts.
How row-level security works#
RLS is a switch on each entity. While it's off, role permissions alone decide access. While it's on, a non-admin user reaches only the records that carry a grant for them.
There are two kinds of grant, both stored per record:
- User grants give one user access to one record.
- Group grants give every member of a group access to one record. They only count while the project's group setting is on (see Turn on group row-level security).
Each grant has a readonly flag. A read-only grant lets the user read the record. A read-write grant also lets them update and delete it. If a user holds several grants on the same record, any read-write grant wins.
What gets filtered#
| Request | Without a grant | With a read-only grant | With a read-write grant |
|---|---|---|---|
GET /items (list) |
Record left out of results and totals | Included | Included |
GET /items/{id} |
404 Not Found |
200 |
200 |
PUT /items/{id} |
Not found | Refused | Updated |
DELETE /items/{id} |
Not deleted | Refused | Deleted |
delete-many |
Skipped | Skipped | Deleted |
The same filter applies to related records expanded into a response, to relationship filters, to record counts behind dashboard charts, and to authenticated search results.
Who bypasses it#
| Caller | Row-level security |
|---|---|
| A user whose role is an administrator role | Bypassed: sees and changes every record |
| A user with any other role | Applied, using their user and group grants |
| An API key | Applied as the user who created the key. A key created by an admin reads everything; a key created by a standard user sees only that user's records. The key's own permissions still decide which entities it can call. |
| Workflows and other Anythink services | Bypassed. Workflow steps read every record, so filter explicitly in the step. |
Role permissions still apply first#
RLS narrows access; it never widens it. A user needs all of these to read a record on an RLS entity:
- A role with API Access turned on. Without it, every data request returns
403. - The entity's
readpermission (for exampleorders:read) on that role. Updates and deletes needupdateanddeletetoo. - A grant on the record.
Field-level permissions are applied to whatever records survive the RLS filter, exactly as they are on entities without RLS.
What happens on create#
When a signed-in user or an API key creates a record on an RLS entity, Anythink grants the creator read-write access automatically. That's why users always see what they've just created.
Records created by a workflow have no user behind them, so they get no automatic grant. Grant access in the step itself (see Set access in workflows) or nobody but administrators will see them.
Creating records isn't limited by RLS. The entity's create permission decides who can create.
Turn on row-level security for an entity#
Turn it on before your app writes data you need to protect. Records created while RLS was off have no grants, so after you switch it on they're hidden from everyone except administrators until you grant access.
In the Anythink dashboard
- Go to Settings › Data Model.
- Select Edit on the entity, or Create Entity for a new one.
- Tick Enable Row-Level Security.
- Select Update Entity (or Create Entity).
The entity now shows an RLS badge in the data model list, and each of its records gets a Row-Level Security tab.
With the CLI
# A new entity
anythink entities create coaching_notes --rls
# An existing entity
anythink entities update coaching_notes --rls true
anythink entities get coaching_notes
── Entity: coaching_notes ────────────────────────────────────────
Table: coaching_notes
Public: no
RLS enabled: yes
With an AI assistant (MCP)
Create a coaching_notes entity with row-level security on, with a title field and an athlete_id integer field.
The assistant uses the cli tool to run entities create coaching_notes --rls, then fields add for each field, and confirms with entities get coaching_notes.
Grant a user access to a record#
Grant access when someone other than the creator needs a record: a support agent on a customer's ticket, or an athlete reading a coach's note. Only project administrators can add, change or remove grants directly. Your app's users can't edit grants on existing records.
In the Anythink dashboard
- Make sure the user's role has API Access and the entity's permissions under Settings › Roles and Permissions.
- Open the record and select the Row-Level Security tab. Only administrators see it, and only on RLS entities.
- Under Users, select Add User Access.
- Choose the User. Read Only Access is ticked by default. Untick it to let the user edit and delete the record.
- Select Add User.
Use the Read Only switch in the users table to change an existing grant between read-only and read-write.
With the CLI
# Read-only
anythink data rls coaching_notes 42 --user 72 --readonly
# Read-write: run again without --readonly to upgrade the same grant
anythink data rls coaching_notes 42 --user 72
✓ RLS access set for user 72 on coaching_notes/42 (readonly: False).
List who has access:
anythink data rls coaching_notes 42
The --readonly flag is off by default, so the CLI grants read-write unless you ask otherwise. The dashboard defaults the other way.
Under the hood these call PUT /org/{org_id}/entities/coaching_notes/items/42/rls-users with { "user_id": 72, "readonly": true }. Calling it again for the same user updates the grant rather than adding a second one. GET on the same path returns the grants:
[
{
"entity_id": 649,
"user_id": 72,
"any_id": 42,
"readonly": true,
"user": { "id": 72, "gid": "…", "first_name": "Sam", "last_name": "Patel", "email": "sam@example.com" }
}
]
With an AI assistant (MCP)
Give user 72 read-only access to coaching note 42, then show me who can see that note.
The assistant runs data rls coaching_notes 42 --user 72 --readonly and then data rls coaching_notes 42 through the cli tool.
Groups#
A group is a named set of users in your project, such as a team, a class, a sales territory or a household. Grant a record to a group once and every current member can reach it. Add someone to the group later and they see every record the group was granted. Remove them and they lose that access at once, with no per-record clean-up.
Turn on group row-level security#
Group grants are off for every project until you turn them on. While the setting is off, Anythink still stores group grants but ignores them, and the Groups section on a record's Row-Level Security tab is hidden. Turning the setting off later stops honouring every group grant immediately; turning it back on restores them.
In the Anythink dashboard
- Go to Settings › Organisation Settings.
- Under Access Controls, tick Enable group-level row access.
- Select Save Changes.
With the CLI
Note: There's no CLI command for this setting. The project update endpoint (PUT /org/{org_id}) replaces every project setting at once, so set it in the dashboard rather than sending a partial update that would reset the others.
With an AI assistant (MCP)
Note: For the same reason, ask your assistant to point you to the dashboard setting rather than change it through fetch.
Create a group and add members#
Each membership has a role. It's one of your project's roles, and it doesn't change the user's project role. It only decides whether that member can make group-scoped reads. For ordinary group grants, any role works.
In the Anythink dashboard
- Go to Settings › Groups and select Create Group.
- Enter a Group Name and an optional Description, then select Create Group.
- Open the group. Under Members, select Add member.
- Choose the User and a Role, or use Create a new role to make one inline.
- Select Add member. Members whose role can read the whole group's data show a Staff badge.
With the CLI
There's no groups command yet, so use fetch against the groups API:
# Create the group
anythink fetch /groups --method POST \
--body '{"name":"Team North","description":"Northern squad"}'
# Add a member with a role (role_id is required)
anythink fetch /groups/4/members --method POST \
--body '{"user_id":72,"role_id":104}'
# List members
anythink fetch /groups/4/members
The member response includes role, can_read_group_data and the user. To remove a member, DELETE /groups/{group_id}/members/{member_id} using the member's id. GET /groups/by-user/{user_id} lists a user's groups, and GET /groups/mine lists the signed-in user's memberships.
With an AI assistant (MCP)
Create a group called Team North and add users 72 and 73 to it with the Standard User role.
The assistant looks up the role id with roles list, then calls fetch /groups --method POST … and fetch /groups/{id}/members --method POST … once per user through the cli tool.
Note: The groups API requires the anythink_groups permissions. Administrators have them. For anyone else, tick them on a role under Settings › Roles and Permissions; they aren't given to any role by default.
Grant a record to a group#
In the Anythink dashboard
- Open the record and select the Row-Level Security tab.
- Under Groups, select Add Group Access. The section only appears once group row-level security is on.
- Choose the Group. Read Only Access is ticked by default.
- Select Add Group.
Sign in as a member of that group and the record appears in their lists. A user outside the group still gets 404.
With the CLI
# Grant
anythink fetch /entities/coaching_notes/items/42/rls-groups --method PUT \
--body '{"group_id":4,"readonly":true}'
# List group grants
anythink fetch /entities/coaching_notes/items/42/rls-groups
[
{
"id": 3,
"entity_id": 649,
"group_id": 4,
"any_id": 42,
"readonly": true,
"group_name": "Team North"
}
]
# Revoke, using the grant's id
anythink fetch /entities/coaching_notes/items/42/rls-groups/3 --method DELETE
With an AI assistant (MCP)
Share coaching note 42 read-only with the Team North group.
The assistant finds the group id with fetch /groups, then calls fetch /entities/coaching_notes/items/42/rls-groups --method PUT --body '{"group_id":4,"readonly":true}' through the cli tool.
Read everything a group can see#
Staff such as coaches, team leads or account managers often need every record that belongs to a group's members, not only the ones granted to the group. Add group_scope to a read:
GET /org/{org_id}/entities/coaching_notes/items?group_scope=4
Authorization: Bearer <user token>
On top of the user's own grants, a group-scoped read returns records that:
- have a user grant for any member of group 4, or
- are granted to group 4.
The caller must be a member of the group whose membership role has the anythink_group_data read permission, or whose membership role is an administrator role (the Staff badge). Project administrators can always use it. Anyone else gets 403 "Your role in group 4 does not allow reading the group's data", rather than a silently narrowed list.
group_scope works on GET /items and GET /items/{id} only. It never grants write access, and it doesn't add group grants, so peers in the group don't see each other's records. That's why it suits teams of peers better than granting every record to the group.
Set access automatically#
On create through the API#
Send an _rls object with the record. Anythink applies it after inserting the record and strips it from the stored data:
POST /org/{org_id}/entities/coaching_notes/items
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Week 3 session plan",
"athlete_id": 72,
"_rls": {
"mode": "add",
"users": [{ "user_id": 72, "readonly": false }],
"groups": [{ "group_id": 4, "readonly": true }]
}
}
| Key | Type | Meaning |
|---|---|---|
mode |
add (default), remove, set |
set replaces every grant on the record with the lists you send. |
users |
list of { user_id, readonly } |
User grants. readonly defaults to false. |
groups |
list of { group_id, readonly } |
Group grants. Stored even while group RLS is off. |
user_groups |
object | Resolve a user's real group memberships on the server (below). |
The creator's own read-write grant is added as well, so a user who creates a record and shares it keeps access. A plain array, "_rls": [{ "user_id": 72, "readonly": false }], is still accepted and means add with those users. POST /items/bulk accepts _rls on each record.
user_groups derives group grants from membership, so a client can't share a record with a group the user isn't in:
"_rls": {
"user_groups": {
"user_id": 72,
"group_id": null,
"readonly": true,
"grant_access": true,
"stamp_field": "team_group_id"
}
}
group_idnulluses every group user 72 belongs to; a number uses only that group, and only if they're a member.grant_access(defaultfalse) writes the group grants. Leave it off when you only want the stamp.stamp_fieldwrites the group id into that field of the record, but only when exactly one group resolves.readonlydefaults totrue.
Any user with the entity's create permission can send _rls, so a user can share a record they create with anyone in the project. Grants on existing records can only be changed by administrators.
Limit: _rls only works on create. PUT /items/{id} drops it without an error, in every mode. Change grants on existing records with the rls-users and rls-groups endpoints.
Set access in workflows#
Create data steps (and Upsert data when it creates a record) have row-level security options, because the records they write have no owner. The values are read from the step's payload after templating:
| Option | Default | What it does |
|---|---|---|
auto_set_rls |
false |
Grant read-write access to the user id(s) in rls_user_field |
rls_user_field |
user |
Payload field holding a user id, a comma-separated list or a list |
auto_set_rls_groups |
false |
Grant read-write access to the group id(s) in rls_group_field |
rls_group_field |
group |
Payload field holding group ids |
auto_set_rls_user_groups |
false |
Resolve the groups the rls_user_field user belongs to |
rls_user_group_mode |
all |
all of their groups, or selected to use rls_user_group_id |
rls_user_group_id |
— | Group id for selected mode. Templated. |
rls_user_group_grant |
false |
Also grant the record to the resolved groups |
rls_user_groups_readonly |
true |
Resolved group grants are read-only |
rls_user_group_field |
— | Field to stamp with the group id when exactly one resolves |
In the step editor these appear as Auto-set RLS, RLS User Field, Auto-set RLS Groups, RLS Group Field, Resolve the User's Groups, Which Groups, Also Grant the Record to the Group, Group Access is Read-only, and Stamp Group Into Field.
Worked example: a coach logs a session and the note is shared with the athlete and, read-only, with the athlete's team. Trigger on EntityCreated for session_logs, then one Create data step:
{
"entity_name": "coaching_notes",
"payload": "{ \"athlete_id\": \"{{ $anythink.trigger.data.athlete_id }}\", \"title\": \"{{ $anythink.trigger.data.summary }}\" }",
"auto_set_rls": true,
"rls_user_field": "athlete_id",
"auto_set_rls_user_groups": true,
"rls_user_group_mode": "all",
"rls_user_group_grant": true,
"rls_user_groups_readonly": true,
"rls_user_group_field": "team_group_id"
}
auto_set_rlsgives the athlete inathlete_idread-write access.auto_set_rls_user_groupswithrls_user_group_grantshares the note read-only with every group the athlete is in. The server looks up the athlete's memberships, so the payload can't name another team.rls_user_group_fieldstampsteam_group_idso you can filter and report by team.- Leave
rls_user_group_grantoff if teammates shouldn't see each other's notes. Coaches can still read them with a group-scoped read.
If a field is missing or doesn't parse as an id, the step logs a warning and writes the record without that grant. See Workflow recipes for more patterns.
Read and filter as a user#
Your app needs nothing RLS-specific to read data. Call the normal endpoints with the signed-in user's token and the results are already filtered:
GET /org/{org_id}/entities/coaching_notes/items?page=1&pageSize=25
Authorization: Bearer <user access token>
total_items and paging count only the records that user can reach, and your filters (?athlete_id=72) run inside that set. To show edit and delete buttons only where they'll work, ask for the user's access to one record:
GET /org/{org_id}/entities/coaching_notes/items/42/permissions
{ "read": true, "create": false, "update": false, "delete": false }
With an API key, the same calls are evaluated as the key's creator, so use user tokens for anything per-user. Use an admin's API key only on a server that should see every record, and never ship one in a client.
Limits and gotchas#
| Behaviour | Detail |
|---|---|
| Admins see everything | Users with an administrator role bypass RLS entirely, including in the dashboard. Test as a standard user. |
| Grant management is admin-only | rls-users and rls-groups endpoints need a project administrator's user token. API keys and app users get 403. |
| Workflow reads ignore RLS | Read data steps see every record. Filter to the user or group you're acting for. |
| Disabling RLS keeps grants | Turn RLS off and everyone with the entity's read permission sees every record; turn it back on and the stored grants apply again. |
| Deleting users and groups | Deleting a user or a group removes their grants. Removing a member from a group ends their group-based access immediately. |
| Search | Authenticated search drops records the user can't read after each page is fetched, so a page can hold fewer hits than pageSize. |
| Cost | Each RLS read adds an indexed EXISTS lookup per grant type. Lists stay fast, but every record needs a grant, so plan for one grant row per user or group per record. |