← Back to Compliance Center
🎭

Role-Based Access Control

Five built-in roles with 40+ granular permissions, brand-level scoping, and custom roles for Enterprise.

Last reviewed 2026-04 · Engineering · Owned by AI Safety Officer

On this page

  1. Overview
  2. Built-in roles
  3. Permissions
  4. Brand-level scoping
  5. Tier gating
  6. Custom roles + overrides
  7. Enforcement
  8. Related documents

Overview

Votriz implements RBAC as Role → Permission → Resource → Action. Every API endpoint that mutates state checks the caller's role + permission set before allowing access. This satisfies SOC 2 CC6.1 (logical access) and gives agencies the brand-level scoping they need to host multiple clients on one Votriz account safely.

Built-in roles

RoleLevelWhat they can do
Owner5Full access. Billing. Audit log export. Cannot be removed. Exactly one per org.
Admin4All features except billing and org deletion. Manages members.
Manager3Manages assigned brands. Approves content. No billing or member management.
Editor2Creates and edits content. Cannot approve, publish, or change settings.
Viewer1Read-only across the dashboard.

The legacy member role is mapped onto Editor's permission set for backward compatibility with accounts created before the role hierarchy expanded.

Permissions

Permissions follow a resource.action shape, with wildcards supported on the right:

content.create # exact verb content.approve # exact verb content.* # all content actions email.* # all email actions * # everything (Owner only)

Selected role → permission map

PermissionOwnerAdminManagerEditorViewer
content.create
content.approve
content.read
email.campaigns.send
billing.manage
members.invite
audit_log.read
audit_log.export

Full map (40+ permissions across 20 resource types) lives in services/permissions.py's ROLE_PERMISSIONS constant.

Brand-level scoping

For agencies hosting multiple clients on one Votriz account, Manager and Editor users can be scoped to specific brands via users_brand_access. A team member assigned to Client X's brand cannot see Client Y's data — even though both clients live in the same agency org.

Owner and Admin always have implicit access to every brand in the org. The scoping is opt-in: orgs that don't add any users_brand_access rows treat managers and editors as unrestricted, which is the right default for single-brand companies.

Tier gating

Some features require a minimum subscription tier regardless of role — an Owner on the Starter plan can't suddenly send email campaigns:

FeatureMinimum tier
Email Marketing (email.*)Growth
SEO Engine (seo.*)Growth
Ghost Presence (ghost.*)Growth
Video Production (video.*)Growth
Brand Monitoring (monitoring.*)Growth
Member invitations (members.invite)Growth
API key management (api_keys.*)Enterprise

Custom roles + overrides

Enterprise customers can define custom roles with bespoke permission lists in custom_roles. The same wildcard patterns work, the same tier gates apply.

Per-user overrides via user_permission_overrides let you grant or deny one specific permission outside the role's default — useful for “this Editor can also approve content for the Q4 launch” without inventing a new role.

Enforcement

Every protected route uses the require_permission() FastAPI dependency:

@router.post("/content/{content_id}/approve") async def approve( content_id: str, user: dict = Depends(require_permission("content.approve")), ): ...

A failed permission check returns HTTP 403 with the required permission name in the error body, and writes a row to security_audit_log with the attempted action.

Related documents

Questions or a custom security review?

Enterprise customers receive dedicated security reviews and direct access to our security team. Reach us anytime at [email protected].

Talk to security →