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

  • aclpolicyname + statements[]. The actual grant.
  • groupname + 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 name only. A label stored on the user; surfaced in the JWT; grants nothing.

A statement

json
{
  "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, POSTcreate, PUTupdate, DELETEdelete. 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.

bash
# 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.