Documentation
IntroductionArchitectureMonorepo & ToolingRunning Locally
Screen Flow MapUser JourneysPersonas & RolesWeb ScreensClient Portal
Convex OverviewData ModelFunctions ReferenceAuth & SessionsRoles & PermissionsJobs & Providers
Web Design SystemAdmin Console DesignPatterns & Conventions
Web AppiOS AppMarketing Website
Testing & HarnessCI Pipeline

Roles & Permissions

Roles & Permissions

Authorization = a minimum-role hierarchy × a per-org policy board. Both live in code once (packages/domain + convex/lib/functions.ts) and are enforced server-side on every function; frontend role gates are UX sugar.

Role hierarchy (ROLE_RANK)
owner
rank 150
manager
rank 100
supervisor
rank 75
employee
rank 50
customer
rank 5
Resolving a policy action
orders.refund
a mutation calls requirePolicy
rolePolicies override?
Settings → Roles, per org
none
POLICY_DEFAULTS
orders.refund → manager
requireRole(minRole)
roleAtLeast(my role, min)?
yes
no
handler runs
ConvexError: FORBIDDEN
Authorization = role rank × per-org policy board — enforced server-side in requirePolicy()

The hierarchy

ROLE_RANK in the domain package — higher rank ⊇ lower rank's defaults:

RoleRank
owner150
manager100
supervisor75
employee50
volunteer20
customer5

roleAtLeast(role, min) compares ranks. requireRole(ctx, min) throws typed FORBIDDEN below the bar.

The policy board

Each sensitive action has a default minimum role (POLICY_DEFAULTS), overridable per org in the rolePolicies table (Settings → Roles):

ActionDefaultGoverns
clients.manageemployeecreate/edit/archive clients, start portal impersonation
clients.view_vipmanagersee (and edit) the VIP flag
pets.manageemployeepets, medication needs, documents
visits.manageemployeebook, edit, cancel, no-show
visits.checkinemployeecheck-in/out, care logging, desk signatures, open issues
visits.checkin_overridemanagercheck in past unsigned agreements; resolve serious issues
orders.manageemployeecreate orders, record payments, fulfillment, dev cards
orders.refundmanagerrefunds (capped per payment)
orders.refund_overrideownerrefund payments older than 30 days
products.managemanagercatalog
notes.managemanagerpost/edit/delete team notes
messages.sendemployeeoutbound client messages
staff.manageownerroster, invites, memberships, schedules
reports.viewemployeeall seven reports
settings.managemanagerevery settings tab
timeclock.approvemanagerapprove/edit time entries

Enforcement: requirePolicy(ctx, action) resolves the effective minimum (org override beats default) then requireRoles it. hasPolicy(ctx, action) is the soft variant used for data shaping — e.g. masking the VIP flag in list payloads rather than throwing.

The three v1-parity grants

The rewrite enforced three grants that in v1 were real permission toggles (commit c7cc36fc, a product decision):

  1. clients.view_vip — VIP is masked in clients.list/listPage/get for roles below the policy, and clients.update refuses to let those roles accidentally wipe a flag they can't see.
  2. orders.refund_override — refunds of payments older than 30 days require it; independently, refunds now cap against each payment's remaining refundable balance, so partial refunds can't re-spend the full amount.
  3. visits.checkin_override — checking in past unsigned required agreements, and resolving serious pet issues ("Resolve Serious Events" parity).

Deliberately not carried over: notes stay broadcast+ack (no per-note permissions), portal retail waits for post-cutover, and the volunteer role is being dropped (still present in the enum).

Self-access rules

Some functions blend policy with ownership:

  • timeclock.entries — employees/volunteers are forced to their own entries regardless of args.
  • staff/[id] accepts the literal id me; self-access works without staff.manage.
  • Portal functions scope everything to the caller's own client record; staff cross that line only via audit-logged impersonation (asClientId + clients.manage).

Verification

The 27-case authz suite (convex/authz.test.ts) covers: staff-data access per role incl. a policy-board override changing the outcome, settings access, platform-owner checks, typed not-found for malformed and cross-tenant ids on every detail read, org resolution errors (unknown slug / bad token / non-member), and time-clock self-access. See Testing & Harness.