Roles & RBACThe six roles

The six roles

Rubberfit’s RBAC matrix has six roles, ordered by privilege. The role string lives on profiles.role, is mirrored into the JWT app_metadata.role claim at sign-in, and is enforced both in middleware (Next.js proxy.ts) and in Postgres RLS policies on every customer-facing table.

The canonical list is the discriminated union in src/application/permissions.ts:

export type AppRole = "admin" | "cutter" | "manager" | "member" | "supervisor" | "viewer"

The matrix

RoleTypical userWhat they can do
adminOperations leadership / workspace ownerFull read/write across the org. Manages users, materials, suppliers, pricing, reorder rules. Reads the admin activity log. Voids cuts.
managerShift managersAll inventory + analytics. Sends + receives POs. Sets reorder rules. Assigns jobs. Cannot manage users or pricing.
supervisorFloor supervisorsApproves cuts. Reads analytics + cut history. Cannot manage suppliers, users, or pricing.
cutterOperators on the floorOperates the cutting engine (Auto Nest + Free-roam). Creates and reads cuts. Reads inventory. Cannot edit material settings.
memberDefault for new usersRead-only inventory access. Promoted into a more specific role once the team has decided how the user fits.
viewerAuditors, accountants, observersRead-only across whatever they’re explicitly granted. Cannot trigger any state change.

The full per-action breakdown is at Permission matrix.

Enforcement

Roles are enforced twice — once in middleware (Next.js proxy.ts) and once in Postgres RLS policies on every customer-facing table.

The middleware check is fast: it reads the JWT, resolves the role from app_metadata, and matches the requested path against the ROUTE_PERMISSIONS map. A cutter who hits /dashboard/inventory/materials/pricing is redirected to /dashboard before any code in that route runs.

The RLS check is slow but airtight: even if the middleware were bypassed (it can’t be — Vercel terminates the request before the route runs — but assume the worst), the database still refuses to return rows the role can’t see. Every policy uses auth.jwt()->'app_metadata'->>'role' to gate access.

This is defense in depth — two layers of enforcement that both have to fail before unauthorized data leaks.

Role transitions

Only admin can promote or demote users. The promotion model is conservative: an admin can grant any role from viewer up through supervisor, but cannot create another admin directly — workspace ownership transfers happen through a deliberate two-step (promote a new admin, then demote the previous one).

Every role change writes to the admin activity log with the action type, actor, and target user.

⚠️

There must always be at least one admin in a workspace. The dashboard prevents the last admin from demoting themselves; if you need to transfer ownership, promote a new admin first, then demote the old one.

See also