Architecture

Roles & permissions

OpsDash uses a four-role RBAC system layered on top of subscription-based feature access. Roles are per-org — a user can be an admin in one workspace and a viewer in another.

The four roles

RoleWhoWhat they can do
ownerCreated the workspace (or received transferred ownership)Everything: billing, members, feature flags, org settings, delete org, transfer ownership
adminTrusted team memberSame as owner except cannot delete org or transfer ownership
memberStandard team memberFull CRUD in CRM, Projects, Forms, Support. No org settings or billing.
viewerRead-only stakeholderView all accessible modules. No create, edit, delete, or export. Enterprise only.

The viewer role is an Enterprise-tier feature (platform:rbac-viewer). On Free and Pro, all team members have full CRUD access.

Action-level gating

ActionAllowed roles
Export (CSV, PDF)owner, admin, member (viewer blocked)
Billing (upgrade, manage subscription)owner, admin only
Feature flags (enable/disable, allowed roles)owner, admin only
Invite members, change roles, suspend membersowner, admin only
Create, edit, delete (CRM, Projects, etc.)owner, admin, member (viewer blocked)
API keys (create, revoke)owner, admin only

Restricting access per feature

Org admins can narrow access per feature via Settings → Admin → Feature Flags. Each feature has an Allowed Roles setting:

  • All roles (default): allowed_roles = null — every role with the feature enabled can access it
  • Specific roles: e.g. allowed_roles = ['owner', 'admin'] — only those roles can access

Server-side enforcement

// Block viewer role from all mutations
const { supabase, org, user } = await withOrg(slug);
await requireNonViewer(supabase, org.id, user.id);

// Block non-admin roles from admin actions
const { supabase, org, user } = await withOrgAdmin(slug);

// Block access if feature flag is disabled or role excluded
await requireFeatureAccess(supabase, org.id, user.id, 'crm:export');

Platform admin

Users listed in PLATFORM_ADMIN_EMAILS have cross-org visibility via the /admin panel. They can view all orgs, users, and subscriptions. This is separate from the org-level admin role and bypasses RLS through the service role client.