Custom Roles
Custom Roles are available in beta on the Convex Business and Enterprise plans.
Custom roles let you define permission policies that go beyond the built-in Admin and Developer roles. A custom role contains a list of statements that grant or deny specific actions on specific resources.
A team member is assigned either a built-in team role (Admin or Developer) or one or more custom roles. Built-in roles and custom roles are mutually exclusive at the team-role level. The Project Admin role is independent of both: a member can hold Project Admin on specific projects regardless of their team-level role, and it grants full access to those projects on top of whatever access the team role provides.
Custom roles are managed on the Team Settings > Custom Roles page in the Convex dashboard. The Convex dashboard offers a number of templates to get you started. We recommend starting with the template that fits your use case best and making edits to meet your needs.
How custom roles are evaluated
When Convex checks whether a member with custom roles can perform an action on a resource, it evaluates each of the member's custom roles independently and combines the results:
- Default deny. If no statement in any role matches the action and resource, access is denied. Custom roles only ever grant access; they never start from "everything is allowed."
- Within a role, deny overrides allow. When a role has both an
allowand adenystatement that match the same action and resource, the role evaluates to denied. Because evaluation short-circuits as soon as a deny is found, the order of allow and deny statements within a role does not matter. The result is the same regardless of how the statements are arranged. - Across roles, allows are combined. The action is allowed if any of the
member's roles evaluates to allowed for it. A
denyin one role does not override anallowin another role.
Project Admin permissions are layered on independently. Even with a restrictive set of custom roles, a member who is a Project Admin on a given project still has full access to that project.
Grammar
A custom role is a list of statements. Each statement is a JSON object with three fields:
{
"effect": "allow",
"actions": ["deployment:view", "deployment:logs:view"],
"resource": "project:*:deployment:type=prod"
}
Statements
A statement grants or denies a set of actions on a set of resources. A custom role must have at least one statement. Each statement is evaluated independently against the action being checked.
Effects
The effect field is one of:
"allow"grants the listed actions on the matching resources."deny"within the same role, blocks the listed actions on the matching resources, overriding anyallowstatement in that role.
Actions
The actions field of a statement is either:
- The string
"*", matching every action that targets the same resource kind as the statement's resource, or - An array of specific action names.
All actions in a single statement must apply to the same top-level
resource kind as the leaf segment of the statement's
resource path. For example, you cannot mix project:view (a project action)
with deployment:view (a deployment action) in the same statement.
For the full list of action names see Role Actions.
Resource specifiers
A resource specifier is a colon-separated path that describes which resources a statement applies to. The path has two kinds of pieces:
- Resource kinds:
team,project,deployment,member,customRole,billing,oauthApplication,sso,integration,defaultEnvironmentVariable, ortoken. - Selectors: filters that follow a kind and narrow which resources of that
kind match. Selectors use
attribute=valuesyntax, likeslug=my-apportype=prod.
The leaf (rightmost) kind of the path determines which actions are valid in the statement.
Symbols
:separates pieces of the path. For example,project:*:deployment:*means "any deployment in any project."*is a wildcard, matching all resources of the given kind.project:*matches every project.=binds a selector attribute to a value, likeid=42. Project, deployment, and member IDs can be looked up through the Platform API.,separates multiple selectors on the same kind. Multiple selectors are OR'd:deployment:type=prod,creator=5matches any deployment that is either a production deployment or created by member 5.
Selectors by kind
Selector support is limited while custom roles is in beta. More selectors will be supported in the future.
| Kind | Selectors |
|---|---|
project | *, id=<project-id>, slug=<project-slug> |
deployment | *, id=<deployment-id>, type=prod|dev|preview|custom, creator=<member-id> |
token | *, creator=<member-id> |
team, member, customRole, billing, oauthApplication, sso, integration, defaultEnvironmentVariable | * only: these resources do not currently support any selectors. |
Nesting
Most resource kinds appear at the top level of a path. A few must be nested:
deploymentmust appear under a project, e.g.project:*:deployment:*.defaultEnvironmentVariablemust appear under a project, e.g.project:*:defaultEnvironmentVariable:*.tokenmust appear under its owning resource (a team, project, or deployment), e.g.team:*:token:*,project:*:token:*, orproject:*:deployment:*:token:*.
Example specifiers
| Specifier | Matches |
|---|---|
team:* | The team itself. |
billing:* | The team's billing settings. |
sso:* | The team's SSO configuration. |
member:* | All team members. |
project:* | All projects in the team. |
project:slug=my-app | The project whose slug is my-app. |
project:*:deployment:* | All deployments in any project. |
project:*:deployment:type=prod | All production deployments. |
project:*:deployment:type=dev,creator=5 | Dev deployments OR deployments created by member 5. |
project:*:defaultEnvironmentVariable:* | Default project environment variables. |
team:*:token:* | All team-scoped access tokens. |
project:*:deployment:*:token:creator=7 | Deployment-scoped access tokens created by member 7. |
Scoping access token actions
There is currently no Platform API for one team member to list team access
tokens created by another member, so granting team:token:view on
team:*:token:* effectively only surfaces the holder's own tokens. The API may
broaden in the future, however, so we recommend scoping token-management actions
to the holder's own tokens by using creator=self . For example, a role that
lets a member manage only their own team tokens would use a resource like
team:*:token:creator=7 rather than team:*:token:*.
Privilege escalation
A few actions effectively grant the ability to escalate privileges, and should be treated as equivalent to full team admin access when included in a custom role.
Be especially careful when granting these actions. Each one can be used to gain permissions the holder doesn't otherwise have, so granting any of them can be effectively the same as granting all Team-Admin or Project-Admin permissions. Only include them in custom roles assigned to people you would otherwise trust at that level.
Membership and roles:
member:invitelets the holder invite any email address to the team, including at the built-in Admin role. A member with this permission can invite themselves at a different email address as a team Admin and gain full team access that way.member:role:updatelets the holder change any other member's team role, including promoting another member (or themselves, indirectly via a second account) to Admin.project:updateMemberRolelets the holder assign Project Admin on a project. Granting this allows the holder to promote themselves to Project Admin, which provides full access to every deployment in that project, including production.
Resource scoping: The built-in roles use a resource's team, project, and deployment type to decide what a member can do (for example, Developer can manage non-prod deployments via team membership, but production deployments require Team Admin or Project Admin). Any action that changes those bindings is a privilege escalation:
deployment:updateTypelets the holder change a deployment's type. A holder who can manage non-prod deployments could create one, then promote it totype=prod, or downgrade an existing prod deployment to a less protected type to operate on it without prod gating.deployment:transfermoves a deployment into another project. If the holder has Project Admin or a more permissive custom role on the destination project, they get that broader access on the transferred deployment.project:transfermoves a project (and all its deployments) into another team. If the holder has Admin or a more permissive custom role on the destination team, they gain that broader access on the transferred project.
Authentication:
sso:updatelets the holder change the team's SSO configuration. The holder could point SSO at an identity provider they control and impersonate other team members at sign-in.sso:disableturns off SSO. Members with alternative sign-in paths could then bypass IdP-enforced policies such as MFA or session controls.
The customRole:create, customRole:update, and customRole:delete actions
have no equivalent in custom-role statements at all: they cannot be granted even
with a wildcard "*", and remain reserved for the built-in Admin role. This
prevents a custom role from defining or escalating itself.
Note on custom role visibility
Listing every custom role on the team requires the customRole:view permission.
However, the Convex dashboard loads a team member's own custom role definitions
regardless of whether they hold customRole:view.
Treat custom role definitions as visible to the members of the team that hold them, and avoid encoding sensitive information in role names or descriptions. For example, if you'd prefer to not disclose a project's slug in the deny rule of a custom role definition, use that project's ID instead.