Custom roles
Field-level RBAC. Each custom role names a set of (content_type, field) read and write grants; assigning a role to a member restricts what they can see and edit in the Studio and over the API.
Module: advanced_rbac (€149/mo) or any of the Regulated, Enterprise, Sovereign presets.
The model
Two layers compose to a final allow/deny decision:
- Enum role baseline (
memberships.role): owner/admin/member can read+write all content; billing reads only. This is the floor. - Custom role overlay (
memberships.custom_role_id→ roles + field_permissions): per(content_type, field)read/write grants.
A custom role can never expand past the enum floor. A billing-role member assigned a custom role with canWrite: true still cannot write — the enum baseline says no.
Deny by adding rows
The engine fallback is allow. A field with no field_permissions row is fully accessible to any custom role assigned to it. Customers add deny rows for the fields they want to restrict.
This is intentional. The opposite default (deny everything, then opt in) means a forgotten role assignment locks a customer out of fields they paid for.
Wildcards
A row with field_name = '*' is the default for the whole content type. Specific names override:
-- Everything in `article` is denied for read, except `title` which is allowed
INSERT INTO field_permissions (role_id, content_type_name, field_name, can_read, can_write)
VALUES
('<role-id>', 'article', '*', false, false),
('<role-id>', 'article', 'title', true, true );
The Studio UI doesn't surface wildcards yet; customers who need this pattern run the SQL directly. A wildcard editor lands in M6.6.4.
Studio UI
Open /settings/roles. Create a role; click into it; uncheck the read or write boxes for the fields you want to deny. Save. Assign the role to a member through /settings/members (M6.8 milestone — earlier customers wire it via SQL).
API behaviour
When a request from an X-Estokad-User who has a custom role assigned hits a content endpoint:
- GET strips denied fields from the response and adds a
_rbac.strippedarray listing the field names. The structure is intentional — the client can render an "unavailable" placeholder rather than crashing onundefined. - POST / PATCH with denied fields in the payload return
403 forbiddenwith a JSON body listing the denied field names.
Every denied write is recorded into the audit chain as rbac.write_denied for forensics.
CLI / system callers
Requests authenticated via MANAGEMENT_API_KEY (the env shortcut for system tooling) bypass RBAC entirely. The header X-Estokad-User is the trigger; without it, no role lookup runs.
Limits
A workspace can have unlimited custom roles. A role can have an unlimited number of field_permissions rows. Performance is engineered for tens of roles and hundreds of permission rows per role; deeper tiers (thousands) require a custom-cost engagement.