Permissions for your app users are enforced only through ACL policies. The app authorizer evaluates them on every request. There is no special admin API — ACL policies, groups, roles, and the links between them are ordinary tenant system entities you manage with the same CRUDL endpoints you use for any entity.
A role is a label, not a permission
A tenant userrole (and the role claim in the JWT) grants nothing. It's a free-form label for your own in-app logic. To change what a user can access, attach an aclpolicy — directly or through a group.
The IAM system entities
- aclpolicy —
name+statements[]. The actual grant. - group —
name+acl_policies[]. A bundle of policies you attach to many users at once. - useraclpolicy — a link:
user_id+acl_policy_id. Attaches one policy directly to one user. - usergroup — a link:
user_id+group_id. Puts a user in a group (they inherit its policies). - userrole — a
nameonly. A label stored on the user; surfaced in the JWT; grants nothing.
A statement
{
"Sid": "EditOrders",
"Effect": "Allow", // Allow | Deny (an explicit Deny always wins)
"Action": ["createorder", "updateorder"], // "<verb><entity>" or a wildcard
"Resource": ["*"], // "*" or a path glob like "/content/*"
"Condition": { // optional, matched on request context
"StringLike": { "request.pathParameters.id": ["abc*"] }
}
} Actions map from the route: GET /list/* → list, GET /item/* → get, POST → create, PUT → update, DELETE → delete. Forms include createorder (verb + entity), a verb wildcard like get*, or * for everything. Evaluation gathers all of a user's statements — direct and via groups — and applies: any matching Deny denies; else any matching Allow allows; else implicit deny.
Build one — worked example
Give a user create/update/delete on order, plus read on everything.
# 1. The policy
POST /app/{orgcode}/create/aclpolicy
{ "name": "order-editor",
"statements": [
{ "Sid": "Read", "Effect": "Allow", "Action": ["get*","list*","count*"], "Resource": ["*"] },
{ "Sid": "Orders", "Effect": "Allow",
"Action": ["createorder","updateorder","deleteorder"], "Resource": ["*"] }
] }
# -> { "id": "<aclId>" }
# 2a. Attach directly to a user ...
POST /app/{orgcode}/create/useraclpolicy
{ "user_id": "<userId>", "acl_policy_id": "<aclId>" }
# 2b. ... or via a reusable group
POST /app/{orgcode}/create/group
{ "name": "editors", "acl_policies": ["<aclId>"] } # -> { "id": "<groupId>" }
POST /app/{orgcode}/create/usergroup
{ "user_id": "<userId>", "group_id": "<groupId>" } Changes take effect on the next token
The authorizer caches a user's resolved ACL per token. After changing policies, have the user re-login (or wait out the cache) to pick them up.
Send only the ids on links
For useraclpolicy and usergroup, send just the id fields — the platform populates the lookup keys the authorizer needs. Never set those by hand.
Defaults that ship with a workspace
When a workspace is created the founder gets an adminacl policy (* on *), the admin role label, and a default-admins group. New members get whatever the workspace's default_member_access names — by default a read-and-self-service vieweracl. Change that default to give joiners a different starting policy.