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
| Role | Typical user | What they can do |
|---|---|---|
admin | Operations leadership / workspace owner | Full read/write across the org. Manages users, materials, suppliers, pricing, reorder rules. Reads the admin activity log. Voids cuts. |
manager | Shift managers | All inventory + analytics. Sends + receives POs. Sets reorder rules. Assigns jobs. Cannot manage users or pricing. |
supervisor | Floor supervisors | Approves cuts. Reads analytics + cut history. Cannot manage suppliers, users, or pricing. |
cutter | Operators on the floor | Operates the cutting engine (Auto Nest + Free-roam). Creates and reads cuts. Reads inventory. Cannot edit material settings. |
member | Default for new users | Read-only inventory access. Promoted into a more specific role once the team has decided how the user fits. |
viewer | Auditors, accountants, observers | Read-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.