There is a bug that almost every product ships at least once. Someone adds a feature, hides the button from users who should not have it, and calls the permission done. The interface looks correct. Every screenshot in the review is right.
Then somebody opens devtools, or bookmarks a URL, or hits the API directly, and discovers the rule was decoration.
What the roles are
A Crixaa workspace has four:
| Role | Can do |
|---|---|
| Admin | Everything, plus members, billing and workspace settings |
| Designer | Create and edit templates, generate documents, comment |
| Reviewer | Comment and decide on reviews; cannot edit templates |
| Viewer | Read only |
The split that matters most is Designer against Reviewer. A reviewer is someone whose judgement you want on a document without handing them the ability to change it — a lawyer, a finance lead, whoever signs off. If reviewing required edit rights, "please check this" would mean "please check this, and by the way you can also rewrite it".
Checked twice, on purpose
The console hides what your role cannot use. The publish action does not render for a viewer, the expiry control does not appear for a reviewer, workspace settings are absent unless you are an admin.
None of that is the permission. It is a courtesy — an interface with no dead ends in it.
The permission is the check on the server, and there are 42 of them across the API today: every route that touches a template, a document, a member or a workspace resolves your membership and asserts a role before it does anything. Delete the class from the DOM, craft the request by hand, replay it from a script — the answer is the same 403.
Those two layers answer different questions:
- The console asks "would this control be useful to you?" and hides it if not.
- The server asks "are you allowed to do this?" and is the only one whose answer counts.
Get that backwards and the security model is a CSS class.
The failure mode when they disagree
Two layers means they can drift, and both directions are worth naming because they feel completely different from the outside.
If the console is more permissive than the server, you get a control that does nothing. The user clicks, gets a 403, and reasonably concludes the product is broken. Embarrassing, but safe.
If the console is more restrictive, nothing appears wrong at all — the feature is just quietly missing for someone entitled to it, and you find out through a support ticket weeks later.
Neither is a security incident, which is exactly why this class of mismatch lingers in products. We treat one as a real bug rather than a cosmetic one, because the alternative is an interface that slowly stops describing the product.
Role is per workspace, not per person
If you belong to more than one workspace, you hold a role in each, independently. Being an admin of your own workspace grants you nothing in a client's.
This catches people out when they switch workspaces mid-session and expect their toolbar to look the same. It will not, and that is the system working: the check that runs on the server resolves your membership in the workspace that owns the thing you are touching, not the one you happen to have open.
API keys are not people
One more boundary worth being explicit about. An API key carries scopes, not a role, and it
never inherits the permissions of whoever created it. A key with documents:generate can
generate documents — that is the whole of its authority, however senior the person who
minted it.
Keys also never carry staff privileges. There is no combination of scopes that reaches an administrative surface, because that path does not exist for key-authenticated requests.
The practical upshot: a leaked key is bounded by its scopes and its workspace. That is what makes rotating one a routine job rather than an incident.
If you are building on this
Assume the server will refuse, and handle the refusal. A 403 is not an exception to log
and move on from — it usually means your integration is scoped differently than you think,
and it will keep happening until someone reads it.
And give integrations their own identity. Reusing a person's credentials for a service means the service inherits that person's blast radius, and it means an offboarding breaks your integration at the worst possible moment.