Authentication Flow
How users prove who they are, how roles work, and how to protect pages from unauthorised access.
Authentication vs Authorisation — The Distinction
These two words are often used interchangeably. They mean different things.
Authentication — Proving who you are. "I am Priya Sharma, here is my password."
Authorisation — What you are allowed to do. "Priya Sharma is a recruiter, so she can post jobs but not access admin reports."
A system without authentication: anyone can access anything. A system with authentication but no authorisation: every logged-in user can access everything. A complete system: users prove who they are, and their role determines what they can do.
Supabase Auth — What It Gives You
Supabase handles the hardest parts of authentication:
- Session management — JWT tokens are issued on login and automatically refreshed before expiry. You don't write any token management code.
- Password hashing — Passwords are stored securely using bcrypt. You never see or store raw passwords.
- Email verification — Supabase sends verification emails and marks accounts as verified.
- Multiple sign-in methods — Email + password, magic link (passwordless), Google OAuth, GitHub OAuth — all with the same API.
- PKCE flow — The most secure auth flow for browser-based apps. Supabase handles the complexity.
What you build on top of Supabase Auth:
- A
profilestable that extends each auth user with your app's data (role, name, onboarding status) - React context that tracks auth state across your app
- Route guards that redirect unauthorised users
- RLS policies that enforce access at the database level
The Profiles Table — Linking Auth to Your App
Supabase manages users in auth.users — a protected system table. Your app data lives in public.profiles. Every user has one row in each.
The link is the id column: when a new user signs up, you create their profiles row immediately after the Supabase auth record is created. The id in both records is identical — this is how you join auth data with app data.
auth.users is Supabase's own locked spreadsheet that tracks authentication details. public.profiles is your own sheet that holds your app's data about the same person. The shared id column is the VLOOKUP that joins them. You never edit auth.users directly — Supabase manages it for you.The AuthContext Pattern — Tracking Auth State
Udyogaseva uses a React Context to make auth state available across the entire app. This is the pattern from src/contexts/AuthContext.tsx:
onAuthStateChange is the key. Supabase calls this callback every time the auth state changes — login, logout, token refresh. Your app always knows the current auth state without polling.
Sign In and Sign Up
After sign out, onAuthStateChange fires and your context updates. All state that depends on user or profile automatically reflects the signed-out state.
Protected Routes — Keeping Unauthorised Users Out
A protected route checks auth state before rendering its children. If the user is not authorised, they are redirected.
The auth check order from the global developer profile — apply this exactly:
Usage in your router:
The Multi-Role Pattern
Udyogaseva has four roles: candidate, recruiter, admin, super_admin. Different users have access to completely different sections of the app.
The role is stored in public.profiles.role. When the user logs in, the profile is fetched and the role determines:
- Which routes they can access (ProtectedRoute checks)
- Which rows they can read/write (RLS policies check)
- Which UI elements appear (conditional rendering in components)
Three layers working together:
- React route guards prevent navigation to wrong pages
- Conditional rendering hides wrong UI elements
- RLS prevents data access even if someone bypasses both
Setting a User's Role
In Udyogaseva, a user chooses their role (candidate or recruiter) during signup. The profile is created with that role:
Admin roles are assigned manually through the admin panel — users cannot give themselves admin access. This is the correct design for any privileged role.
Checking Auth in Components
Throughout your app, you need to know the current user's identity and role.
Never call supabase.auth.getUser() inside a component. That makes a network request on every render. Use the useAuth() hook which returns the cached auth state from context.
useAuth() hook that reads from your Supabase auth context. Then add a conditional in any component: if the user's role is admin, show an extra action button. This is conditional rendering based on auth state — the most common pattern in multi-role apps.Sentry Integration for Auth Events
In production apps, track auth events in Sentry so you know who was affected by errors:
When an error occurs in production, Sentry records which user experienced it — including their role. This makes debugging "the recruiter dashboard is broken" much faster when you can see exactly which recruiter accounts triggered the error.
- I can explain authentication vs authorisation using the Excel analogy — opening the file vs sheet-level permissions
- I understand the profiles table pattern: auth.users is Supabase's locked table, public.profiles is my app's extended data linked by the same id
- I know why onAuthStateChange is used instead of calling getUser() inside components
- I understand the 3-layer security model: route guards + conditional rendering + RLS — all three required
- I know that admin roles must be assigned manually — users cannot grant themselves elevated access
- I understand that a URL alone provides zero security — a protected route must check authentication before rendering any content