Role Permission Matrix
Complete matrix of what each role can do with each subject, including personal org overrides and the unknown-role fallback.
This page documents the complete permission matrix derived from defineAbilityFor() in packages/api/src/abilities/define-ability.ts.
Roles
Trovella has three defined roles plus a safe default:
| Role | Description |
|---|---|
| Owner | Full control. One per organization. Cannot be demoted via the API. |
| Admin | Manages the organization, its members, and invitations. Cannot delete the organization. |
| Member | Reads org metadata, creates and manages research plans and artifacts. No member management. |
| Default | Fallback for unknown/corrupted role values. Read-only access to org and member data. |
Company and Family Organizations
| Subject | Action | Owner | Admin | Member | Default |
|---|---|---|---|---|---|
| Organization | read | Yes | Yes | Yes | Yes |
| Organization | update | Yes | Yes | -- | -- |
| Organization | delete | Yes | -- | -- | -- |
| Member | read | Yes | Yes | Yes | Yes |
| Member | create | Yes | Yes | -- | -- |
| Member | update | Yes | Yes | -- | -- |
| Member | delete | Yes | Yes | -- | -- |
| Invitation | read | Yes | Yes | Yes | -- |
| Invitation | create | Yes | Yes | -- | -- |
| Invitation | update | Yes | Yes | -- | -- |
| Invitation | delete | Yes | Yes | -- | -- |
| ResearchPlan | read | Yes | Yes | Yes | -- |
| ResearchPlan | create | Yes | Yes | Yes | -- |
| ResearchPlan | update | Yes | Yes | Yes | -- |
| ResearchPlan | delete | Yes | -- | -- | -- |
| ResearchArtifact | read | Yes | Yes | Yes | -- |
| ResearchArtifact | create | Yes | Yes | Yes | -- |
| ResearchArtifact | update | Yes | Yes | Yes | -- |
| ResearchArtifact | delete | Yes | -- | -- | -- |
Notes:
- Owner gets
manage all, which CASL expands to every action on every subject (including future subjects). The explicit "Yes" entries in the owner column are a consequence of this wildcard. - Admin gets
manage Invitation, which CASL expands to all four CRUD actions on invitations. - Admin does not get explicit research plan/artifact permissions. Those subjects are not mentioned in the admin case. However, if an admin also holds a member role (not currently possible -- roles are exclusive), they would inherit member permissions.
- Member gets
read Invitationbut notcreate,update, ordelete. Members can see who has been invited but cannot manage invitations.
Personal Organizations
Personal organizations apply cannot() overrides after the role switch:
| Subject | Action | Effect |
|---|---|---|
| Member | create | Denied regardless of role |
| Invitation | manage | Denied regardless of role (all CRUD) |
This means an owner of a personal organization retains manage all for everything except member creation and invitation management. All other permissions remain unchanged.
Personal orgs are single-user accounts. The collaboration features (members and invitations) are disabled at the authorization layer, not just the UI.
ABAC Conditions (Router-Level)
The matrix above covers RBAC -- what the CASL ability object permits. Some operations have additional attribute-based restrictions enforced in router code:
| Router | Condition | Effect |
|---|---|---|
member.updateRole | Target member has role "owner" | Blocked: "Cannot change an owner's role" |
member.remove | Target member has role "owner" | Blocked: "Cannot remove the organization owner" |
member.remove | Caller is the target member | Allowed: members can remove themselves even without delete Member permission |
These ABAC conditions are not encoded in the CASL ability -- they are explicit if statements in the router handler. See Router Enforcement for details on why and how.
How to Read This Matrix Against Code
The source of truth is defineAbilityFor() in packages/api/src/abilities/define-ability.ts. If this matrix and the code disagree, the code wins.
To verify a specific permission:
import { defineAbilityFor } from "./abilities";
const ability = defineAbilityFor({
userId: "test-user",
role: "admin",
orgType: "company",
});
ability.can("delete", "Organization"); // false
ability.can("update", "Organization"); // true
ability.can("manage", "Invitation"); // true
The test file at packages/api/src/abilities/__tests__/define-ability.test.ts contains assertions for every cell in this matrix.