Role-Based Access Control System

Comprehensive RBAC system with 5 roles, 10 permissions, role-based route protection, role-aware navigation, admin role management, and scoped quotes visibility via RLS policies.

Overview

This release introduces a full role-based access control system that governs who can access what across the entire platform. Five roles define distinct access levels — from customers who only see the public catalogue, to designers who manage the builder, sales reps who handle quotes, and admins who oversee everything. Super-admins retain unrestricted access through both /home and /admin.

Roles & Hierarchy

The system defines five roles ordered by privilege level (lower number = more authority):

LevelRoleDescription
1OwnerSystem role with all 10 permissions
2AdminFull platform management — products, designs, assets, quotes, users, billing
3DesignerBuilder setup — products, designs, assets. No access to quotes
4Sales RepQuote management — view and manage own quotes only
5MemberCustomer — public catalogue and uniform builder only. Cannot access /home

A sixth access level, Super Admin, is not stored in the database but determined by the user's JWT (app_metadata.role = 'super-admin' + MFA). Super-admins bypass all route restrictions.

Permissions Matrix

Ten granular permissions control feature access:

PermissionOwnerAdminDesignerSales RepMember
roles.manageYes
billing.manageYesYes
settings.manageYesYesYesYesYes
members.manageYesYes
invites.manageYesYes
products.manageYesYesYes
designs.manageYesYesYes
assets.manageYesYesYes
quotes.manageYesYesYes
rules.manageYesYes

Route Access Control

RouteMemberSales RepDesignerAdminSuper Admin
/ (catalogue)YesYesYesYesYes
/builder/*YesYesYesYesYes
/homeNoYesYesYesYes
/home/productsNoNoYesYesYes
/home/designsNoNoYesYesYes
/home/assets/*NoNoYesYesYes
/home/quotesNoYesNoYesYes
/home/settingsNoYesNoYesYes
/adminNoNoNoNoYes

Features

Middleware Route Protection

The Next.js middleware (proxy.ts) enforces access to /home/* at the edge:

  1. Checks authentication — unauthenticated users redirect to sign-in
  2. Checks MFA requirement — redirects to verification if needed
  3. Checks super-admin status — super-admins always pass
  4. Calls get_account_role() RPC — staff roles (admin, designer, sales-rep) pass; customers (member) redirect to /

Customer-First Sign-In Flow

All post-login redirects now default to / (the product catalogue) instead of /home. This applies to:

  • Email/password sign-in
  • OAuth (Google) sign-in
  • Sign-up
  • Email confirmation
  • MFA verification

Staff members navigate to /home manually or via direct links. The middleware validates their role before granting access.

Role-Aware Navigation

The /home sidebar dynamically shows only the routes relevant to the user's role:

  • Admin sees: Home, Products, Designs, Assets (Colors, Palettes, Fonts, Graphics), Quotes, Settings
  • Designer sees: Home, Products, Designs, Assets
  • Sales Rep sees: Quotes, Settings

The navigation config accepts the user's role from the user_account_workspace database view and filters routes accordingly.

Admin Panel Enhancements

The /admin sidebar now includes a "Content" section with direct links to /home/products, /home/designs, /home/assets, and /home/quotes — giving super-admins quick access to content management.

The accounts table at /admin/accounts gains:

  • A Role column displaying each personal account's current role
  • A Change Role action in the account dropdown, allowing super-admins to promote or demote users between member, admin, designer, and sales-rep

Quotes Row-Level Security

Database-level RLS policies now scope quote visibility by role:

  • Admin/Owner (hierarchy level 1–2): see all quotes in the account
  • Sales Rep: see only quotes they personally created
  • Member: see only their own submitted quotes (read-only)
  • Designer: no quote access whatsoever

Two helper functions — can_view_quote() and can_update_quote() — encapsulate the logic, keeping RLS policies clean and maintainable.

Server Action Guards

A reusable requireTeamPermission() utility enables permission checks inside any server action. Quote mutation actions (delete, update fulfillment, update notes) now accept an optional accountId parameter — when provided, the caller's quotes.manage permission is verified before proceeding.

Technical Details

New Files

apps/web/
├── lib/server/require-team-permission.ts    # Reusable permission guard

packages/features/admin/src/
├── components/admin-change-role-dialog.tsx   # Role assignment UI
└── lib/server/schema/admin-actions.schema.ts # ChangeAccountRoleSchema

Modified Files (29 total)

Middleware & Auth (7 files):
├── proxy.ts                                 # Role check in /home handler
├── auth/sign-in/page.tsx                    # Redirect default → /
├── auth/sign-up/page.tsx                    # Redirect default → /
├── auth/callback/route.ts                   # Redirect default → /
├── auth/confirm/route.ts                    # Redirect default → /
├── auth/verify/page.tsx                     # Redirect default → /
└── sign-in-methods-container.tsx            # Fallback redirect → /

Navigation (8 files):
├── personal-account-navigation.config.tsx   # Role-aware filtering
├── team-account-navigation.config.tsx       # Permission-aware filtering
├── home-sidebar.tsx                         # Passes role to config
├── home-mobile-navigation.tsx               # Passes role to config
├── home-menu-navigation.tsx                 # Passes role to config
├── team-account-layout-sidebar.tsx          # Passes permissions to config
├── team-account-layout-mobile-navigation.tsx
└── team-account-navigation-menu.tsx

Admin (4 files):
├── admin-sidebar.tsx                        # Content links section
├── admin-accounts-table.tsx                 # Role column + Change Role action
├── admin-server-actions.ts                  # changeAccountRoleAction
└── admin-actions.schema.ts                  # ChangeAccountRoleSchema

Schema (4 files):
├── 01-enums.sql                             # Extended app_permissions
├── 03-accounts.sql                          # role column + trigger update
├── 15-account-views.sql                     # View includes role
└── 17-roles-seed.sql                        # Full 5-role seed

Other (4 files):
├── [account]/layout.tsx                     # Passes permissions to nav
├── quote.schema.ts                          # Optional accountId
├── quote server-actions.ts                  # Team permission guard
└── database.types.ts                        # Updated TypeScript types

Data Flow

  1. User signs up → kit.setup_new_user() trigger creates personal account with role = 'member'
  2. Super-admin visits /admin/accounts → changes role to designer, sales-rep, or admin via dropdown
  3. User signs in → lands on / (product catalogue) regardless of role
  4. User navigates to /home → middleware calls get_account_role() → member redirected to /, staff allowed through
  5. /home layout loads user_account_workspace view (includes role) → sidebar filtered by role
  6. Quote operations check can_view_quote() / can_update_quote() at the database level for row-scoped access