Role-Based Access Control
Five built-in roles with 40+ granular permissions, brand-level scoping, and custom roles for Enterprise.
On this page
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
| Role | Level | What they can do |
|---|---|---|
| Owner | 5 | Full access. Billing. Audit log export. Cannot be removed. Exactly one per org. |
| Admin | 4 | All features except billing and org deletion. Manages members. |
| Manager | 3 | Manages assigned brands. Approves content. No billing or member management. |
| Editor | 2 | Creates and edits content. Cannot approve, publish, or change settings. |
| Viewer | 1 | Read-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:
Selected role → permission map
| Permission | Owner | Admin | Manager | Editor | Viewer |
|---|---|---|---|---|---|
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:
| Feature | Minimum 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:
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 →