Permissions & Auth UI Standard
Backend enforces, frontend reflects. Hide vs disable decision, reading role from auth session, page-level access, partial access, auth flow standards.
The frontend reflects permissions. The backend enforces them. Never enforce a rule only on the frontend.
Contents
| # | Section |
|---|---|
| 1 | The Fundamental Rule |
| 2 | Hide vs Disable |
| 3 | Reading Role & Permissions |
| 4 | UI Element Permission States |
| 5 | Page-Level Access |
| 6 | Partial Access |
| 7 | Real-Time Role Changes |
| 8 | Auth Flow Standards |
1. The Fundamental Rule
Frontend → reflects permissions (hides/disables UI elements)
Backend → enforces permissions (rejects unauthorized requests)
If the backend does not enforce a rule, a technical user can bypass the frontend.
Frontend-only permission checks are a UX feature — not a security feature.
2. Hide vs Disable
| When to Hide | When to Disable |
|---|---|
| The user can never do this action regardless of context | The user could do this action after meeting a condition |
| The element is irrelevant to the user's role | The element is relevant but currently blocked |
| Showing it would be confusing or noisy | Showing it helps the user understand what's available |
HIDE WHEN:
[ ] The user's role has no access at all (e.g., Viewer cannot see Delete button anywhere)
[ ] The feature is not part of their plan/tier
[ ] Showing would expose information they shouldn't see
DISABLE WITH TOOLTIP WHEN:
[ ] The user is on a free plan and could upgrade
[ ] The user's role is limited but they might need to request access
[ ] The action requires additional conditions to be met (e.g., form is incomplete)
[ ] The button is disabled during loading
TOOLTIP TEXT FOR DISABLED:
[ ] Explains why: "Admin access required to delete projects."
[ ] Does NOT just say "Disabled"
[ ] Does NOT show a tooltip if the reason is obvious
3. Reading Role & Permissions
[ ] Role is always read from the backend auth session — never from URL params or localStorage alone
[ ] Role is never passed in the request body from the frontend
[ ] The role is part of the authenticated user's session (JWT claim or session lookup)
[ ] If the role changes server-side, the frontend reflects it on next session refresh
[ ] Never trust a role stored only in a frontend variable — re-verify with the backend on sensitive actions
SUPABASE PATTERN:
const { data: { user } } = await supabase.auth.getUser();
const role = user?.app_metadata?.role; // from JWT, set by backend
REACT PATTERN:
// Read from auth context — populated on login from backend response
const { user } = useAuth();
const isAdmin = user?.role === 'admin';
4. UI Element Permission States
BUTTONS
[ ] Admin-only action buttons: hidden for non-admins (or disabled with tooltip)
[ ] Danger actions (Delete, Archive): hidden for non-owners/admins
[ ] Edit button: visible to editors and above; hidden for viewers
FORM FIELDS
[ ] Read-only for users without edit permission (not disabled — use a read view)
[ ] If showing a form to a viewer: show a read-only view, not a disabled form
[ ] Never show a submit button to a user who cannot submit
NAVIGATION
[ ] Admin sections (Settings, Team, Billing): hidden in nav for non-admins
[ ] Not just a dead link — completely absent from navigation
[ ] 403 page shown if user navigates to admin URL directly
TABLE ROW ACTIONS
[ ] Delete column: hidden entirely for non-admins
[ ] Edit column: hidden for viewers
[ ] Role-appropriate actions only per row (e.g., cannot archive own account)
BULK ACTIONS
[ ] Bulk delete: available only to admin/owner roles
[ ] Bulk export: permission-gated per plan or role
5. Page-Level Access
6. Partial Access
Some pages are visible to multiple roles but with different capability levels.
7. Real-Time Role Changes
— Their session reflects the new role on the next page navigation or explicit refresh
— They are not shown actions they can no longer perform
— Their next API call returns 403/FORBIDDEN
— Frontend catches this and redirects to a "You've been removed" state
8. Auth Flow Standards
LOGIN
[ ] Login form: email + password (or OAuth)
[ ] "Remember me" checkbox (default: session-length only)
[ ] Failed login: "Incorrect email or password." — same message for both (no enumeration)
[ ] Account lockout after N failed attempts: "Too many attempts. Try again in 15 minutes."
[ ] "Forgot password" link visible on login form
SIGNUP
[ ] Email verification sent immediately after signup
[ ] "Check your email" state shown — not a redirect to the app
[ ] User cannot access the app until email is verified (if required)
[ ] Duplicate email: "An account with this email already exists." + "Sign in" link
SESSION
[ ] Session expiry: redirect to login with "Your session has expired." message
[ ] Token refresh handled silently — no user-visible flash
[ ] After login: redirect to the original destination (not always the dashboard)
LOGOUT
[ ] Clears all local session data (tokens, cached user data)
[ ] Redirects to login page
[ ] Never shows the user their previous data after logout
Practice Task
→ Permissions & Role-Based UI Checklist Review the checklist and apply it to any page that renders different content based on the user's role.