Auth Flow¶
This page describes how authentication works end-to-end in Workbench AI: from login/signup to role resolution and route gating.
Overview¶
Authentication is handled by Supabase Auth with a thin application layer on top:
- Supabase Auth manages users, sessions, and provider flows.
- A
user_profilestable stores application-level roles and tenant/company links. - The frontend uses
AuthProvider+useAuth()to expose user, role, and tenant info. - Routes are gated with
ProtectedRoute, which checks:- Logged-in state
- Role (
owner,tenant,admin) - Subscription status and feature access
UI pages¶
Login (/login → src/pages/auth/Login.tsx)¶
Responsibilities:
- Collects email and password.
- Calls
supabase.auth.signInWithPassword({ email, password }). - On success:
- Calls
supabase.rpc('get_tenant_info_for_user', { user_id_param: data.user.id })to fetch: - Role (
owner/tenant/admin) tenant_id- Optionally fetches
company.namefromcompaniesusingtenant_id. - Writes a snapshot of this profile to
localStorageundersb-user-profile. - Redirects based on role:
owner→/owner-dashboard- others →
/dashboard
- Calls
If the user is already logged in (via useAuth()), Login immediately redirects to the appropriate dashboard using getRedirectPath(role).
Signup (/signup → src/pages/auth/Signup.tsx)¶
Responsibilities:
- Collects account + company info via
SignupForm. - Coordinates with:
useSignup()– creates Supabase user and initial tenant/company records.useCheckout()– starts Stripe checkout for the selected plan.useSignupSession()– keeps signup state across page reloads.
- Flow:
- User selects a plan on
/pricing(stored in query paramplan). - User fills signup form and submits.
handleSignupcreates user + tenant/company and storessignupData.TrialModalconfirms trial/plan; on agree,handleCheckoutstarts Stripe checkout.- After checkout, the auth callback and Stripe flows determine the final redirect.
- User selects a plan on
Auth callback (/auth/callback → src/pages/auth/AuthCallback.tsx)¶
Responsibilities:
- Handles cases where Supabase returns the user to the app with hash or query params (PKCE flow).
- Steps:
- On mount, checks
window.location.hashforaccess_token/error. - Calls
supabase.auth.getSession()to let Supabase process the callback and populate session. - Clears the URL hash.
- On mount, checks
- After Supabase session is established:
- Waits on
useAuth()to loaduser+role. - If no
user→ redirect to/login. - If
userbutroleis not yet available: - After a small timeout, directly calls
supabase.rpc('get_tenant_info_for_user', ...)and redirects based on role. - If
roleexists: - Checks
sessionStorage.getItem('selectedPlan')(set during signup) and, if present, redirects to/upgrade?plan=...&source=signup. - Otherwise, redirects using
getRedirectPath(role).
- Waits on
Clear session (/clear-session → src/pages/auth/ClearSession.tsx)¶
Responsibilities:
- Provides a hard reset of local auth state.
- Flow:
- Preserves Rewardful tracking data (via
preserveRewardfulData()). - Clears
localStorageandsessionStorage. - Restores Rewardful data to avoid breaking referral tracking.
- Calls
supabase.auth.signOut(). - Redirects to
/loginafter a short delay.
- Preserves Rewardful tracking data (via
This route is useful for support/debugging scenarios where a user’s local session is corrupted.
AuthProvider and session handling¶
File: src/contexts/AuthContext.tsx
The AuthProvider wraps the app and exposes:
user: Supabaseauth.usersrecord.session: current Supabase session.tenantId: current tenant/company ID.companyName: human-friendly company name.role:'owner' | 'tenant' | 'admin' | null.loading: whether auth info is being resolved.
Role & tenant resolution¶
A helper fetchTenantInfo(userId, useStoredOnly) implements the unified resolution flow:
-
Read stored profile (optional)
- Tries
localStorage['sb-user-profile']for a cached{ userId, role, tenantId, companyName }. - If
useStoredOnlyistrueand a matching profile exists, uses it and skips DB calls.
- Tries
-
Fetch from database (normal path)
- Calls
supabase.rpc('get_tenant_info_for_user', { user_id_param: userId })with retry + timeout logic. - Uses the RPC’s result to populate
roleandtenantId. - Fetches
company.namefromcompaniesusingtenantId. - Writes the profile back to
localStoragefor next time.
- Calls
-
Fallbacks
- If RPC or DB lookups fail but a stored profile exists, use the stored profile.
- If neither DB nor stored profile is available, clears role/tenant/company and clears stored profile.
Lifecycle¶
On app startup / refresh:
AuthProviderreads the current session via Supabase.- For a logged-in user:
- First tries
fetchTenantInfo(userId, useStoredOnly = true)to use cached data. - If needed, falls back to a full fetch.
- First tries
- For guests:
userandrolearenull,loadingeventually becomesfalse.
Routes and components consume this via the useAuth() hook.
Route gating (ProtectedRoute)¶
File: src/components/ProtectedRoute.tsx
ProtectedRoute is a wrapper used in App.tsx for all protected routes.
Checks performed:
-
Loading
- If auth or subscription state is loading, renders a full-screen
Loading...view.
- If auth or subscription state is loading, renders a full-screen
-
Authentication
- If
userisnull, redirects to/login.
- If
-
Role-based access
- If
requiredRoleis set andhasRequiredRole(role, requiredRole)is false, redirects to/dashboard.
- If
-
Owner bypass
- If
isOwnerorrole === 'owner', returnschildrenwithout subscription checks.
- If
-
Subscription & feature gating (non-owner)
- If no subscription exists → redirect to
/billingwithstate.reason = 'no_subscription'. - If subscription is inactive → redirect to
/billingwith a lockout reason fromgetLockoutReason(subscription). - If a
featureis specified andcanAccess(feature)is false:- Renders an "Upgrade Required" card with
UpgradePromptto encourage upgrading to the necessary plan.
- Renders an "Upgrade Required" card with
- If no subscription exists → redirect to
Public vs protected routes
- Public/auth routes like
/,/pricing,/login,/signup,/auth/callback,/clear-sessionare not wrapped inProtectedRoute. - Dashboard and tools (e.g.
/dashboard,/orders,/clients,/design/*,/payments/*) are wrapped and enforced via this component.
End-to-end flow¶
Login & role resolution¶
flowchart LR
U[User Browser] --> LOGIN[/Login page /]
LOGIN -->|email + password| SBAUTH[Supabase Auth]
SBAUTH -->|session| SPA[AuthProvider]
SPA -->|RPC: get_tenant_info_for_user| SBD[(Supabase DB)]
SBD --> SPA
SPA --> ROUTES[Protected routes]
click LOGIN "../architecture/frontend/" "Frontend auth pages (Login)"
click SBAUTH "../architecture/supabase/" "Supabase auth & roles"
click SBD "../architecture/supabase/" "Supabase schema overview"
click ROUTES "../architecture/frontend/" "Route map & ProtectedRoute"
Narrative:
- User submits login form on
/login. supabase.auth.signInWithPasswordcreates/returns a session.AuthProvider(or the login page) usesget_tenant_info_for_userto resolve tenant and role from the DB.- Role, tenant, and company info are cached in localStorage and stored in context.
ProtectedRouteuses this context to gate access to protected routes.
Signup + callback + upgrade (high level)¶
flowchart LR
PRICING[/Pricing page/] --> SIGNUP[/Signup page/]
SIGNUP -->|create user + tenant| SBAUTH[Supabase Auth]
SIGNUP -->|start checkout| ST[Stripe Checkout]
ST -->|redirect on success| CALLBACK[/AuthCallback/]
CALLBACK --> SPA[AuthProvider]
SPA -->|role + tenant| ROUTES[Protected routes]
click SIGNUP "../architecture/frontend/" "Signup page & flows"
click SBAUTH "../architecture/supabase/" "Supabase auth & roles"
click ST "./billing-stripe/" "Billing & Stripe flows"
click CALLBACK "../architecture/frontend/" "AuthCallback route"
- The signup flow creates accounts in both Supabase Auth and the app’s schema.
- Stripe handles payment details and trial logic.
- Auth callback and
AuthProviderensure the session is valid and the user is redirected to the correct dashboard or upgrade flow.
Where to look next¶
- For subscription and billing details: Billing & Stripe Flow
- For Stripe webhook handling and how subscription state is mirrored to Supabase: Stripe Webhooks Flow
- For a broader system view: Architecture → Supabase and Architecture → Frontend