Authentication Flow
Complete guide to how authentication works in the multi-tenant system.
Overview
Authentication in Lager Guru determines both user identity and tenant context. The system uses Supabase Auth for authentication and tenant_users table for tenant assignment.
Authentication Flow Diagram
flowchart TD
A[User Login] --> B[Supabase Auth<br/>Email/Password]
B --> C{Authentication<br/>Successful?}
C -->|No| D[Show Error]
C -->|Yes| E[Get User Profile]
E --> F[Lookup Tenant<br/>tenant_users table]
F --> G{Tenant<br/>Found?}
G -->|No| H[Show No Tenant Message]
G -->|Yes| I[Set Tenant Context]
I --> J[Load User Role<br/>user_roles table]
J --> K[Set Application State]
K --> L[Redirect to Dashboard]
style A fill:#3b82f6,color:#fff
style B fill:#10b981,color:#fff
style I fill:#f59e0b,color:#fff
style L fill:#8b5cf6,color:#fffStep-by-Step Flow
1. User Login
User provides credentials on login page:
// src/pages/Login.tsx
const { signIn } = useAuth();
const handleSignIn = async (email: string, password: string) => {
const { error } = await signIn(email, password);
// Supabase Auth handles authentication
};2. Supabase Auth Verification
Supabase Auth verifies credentials and creates session:
// src/contexts/AuthContext.tsx
const { data: { session } } = await supabase.auth.signInWithPassword({
email,
password,
});Result: User session is created with:
user.id: User UUIDuser.email: User emailsession.access_token: JWT token
3. Fetch User Profile
After authentication, fetch user profile:
// Automatically handled by Supabase
// Profile is created via trigger when user signs up
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();4. Determine Tenant Context
Look up user's tenant assignment:
// src/lib/tenants.ts
export async function getCurrentUserTenantId(): Promise<string | null> {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return null;
const { data } = await supabase
.from('tenant_users')
.select('tenant_id')
.eq('user_id', user.id)
.maybeSingle();
return data?.tenant_id || null;
}Note: Users are typically assigned to one tenant. If multiple tenants are supported, the first one is used or user selects.
5. Fetch User Role
Get user's global role:
// src/contexts/AuthContext.tsx
const fetchUserRole = async (userId: string) => {
const { data } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId)
.maybeSingle();
return data?.role || null; // 'admin', 'driver', or 'worker'
};6. Set Application Context
Store authentication and tenant context:
// AuthContext provides:
- user: User object from Supabase Auth
- session: Current session
- userRole: Global role ('admin', 'driver', 'worker')
- tenantId: Current tenant ID (from tenant_users)Session Management
Session Storage
Supabase stores session in localStorage:
// Automatically handled by Supabase client
const supabase = createClient(url, key, {
auth: {
storage: localStorage,
persistSession: true,
autoRefreshToken: true,
},
});Session Refresh
Sessions are automatically refreshed:
// Supabase handles this automatically
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
// Session refreshed
}
});RLS Enforcement
Once authenticated, all queries are automatically filtered by RLS:
// This query is automatically filtered by RLS
// User can only see their tenant's shipments
const { data } = await supabase
.from('shipments')
.select('*');
// RLS policy ensures only tenant's data is returnedHow RLS Works
- JWT Token: Contains
user.id(auth.uid()) - Policy Evaluation: PostgreSQL evaluates RLS policies using
auth.uid() - Automatic Filtering: Queries are filtered based on tenant_users table
- Transparent: Application code doesn't need explicit filtering
SSO Authentication Flow
For enterprise SSO (OIDC/SAML), the flow is similar but uses external provider:
sequenceDiagram
participant U as User
participant A as Lager Guru App
participant S as SSO Provider
participant D as Database
U->>A: Click SSO Login
A->>S: Redirect to SSO Provider
S->>U: Show Login Form
U->>S: Enter Credentials
S->>A: Callback with User Info
A->>D: Auto-Provision User & Tenant
D->>A: User Created/Updated
A->>A: Create Session
A->>U: Redirect to DashboardSee Enterprise SSO Documentation for details.
Logout Flow
// src/contexts/AuthContext.tsx
const signOut = async () => {
await supabase.auth.signOut();
setUserRole(null);
navigate('/login');
};What Happens:
- Supabase session is cleared
- LocalStorage is cleared
- User is redirected to login
- Tenant context is lost
Route Protection
Protected routes check authentication and tenant:
// src/App.tsx
const ProtectedRoute = ({ children, allowedRoles }) => {
const { user, userRole, loading } = useAuth();
if (loading) return <Loading />;
if (!user || !userRole) return <Navigate to="/login" />;
if (!allowedRoles.includes(userRole)) return <Navigate to="/login" />;
return <>{children}</>;
};Tenant Context in Queries
Automatic Filtering
RLS automatically filters by tenant, but you can also explicitly filter:
// Explicit tenant filtering (optional, RLS also does this)
const tenantId = await getCurrentUserTenantId();
const { data } = await supabase
.from('shipments')
.select('*')
.eq('tenant_id', tenantId); // Explicit filterHelper Functions
// Get tenant-scoped query builder
async function getTenantQuery<T>(table: string) {
const tenantId = await getCurrentUserTenantId();
if (!tenantId) throw new Error('No tenant context');
return supabase.from(table).eq('tenant_id', tenantId);
}Error Scenarios
User Has No Tenant
const tenantId = await getCurrentUserTenantId();
if (!tenantId) {
// User is authenticated but not assigned to tenant
// Show message: "Please contact administrator"
return <NoTenantAssigned />;
}User Has No Role
const { userRole } = useAuth();
if (!userRole) {
// User is authenticated but has no role
// Show message: "Role assignment pending"
return <RoleAssignmentPending />;
}Session Expired
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT' || !session) {
// Redirect to login
navigate('/login');
}
});Security Considerations
1. Token Security
- JWT tokens are stored in localStorage (consider httpOnly cookies for production)
- Tokens are automatically refreshed
- Tokens expire and require re-authentication
2. RLS Enforcement
- All queries are automatically filtered by RLS
- Service role is never used for user requests
- Tenant context is always verified
3. Session Validation
// Verify session is valid
const { data: { session }, error } = await supabase.auth.getSession();
if (error || !session) {
// Session invalid, redirect to login
navigate('/login');
}Best Practices
- Always Check Authentication: Verify user is authenticated before operations
- Verify Tenant Context: Ensure tenant_id is set before data operations
- Handle Errors Gracefully: Provide clear messages for auth failures
- Auto-Refresh: Let Supabase handle token refresh automatically
- Logout on Errors: Clear session on critical errors
Related Documentation
- RLS Policies - How RLS enforces tenant isolation
- Tenant Switching - Switching between tenants
- Service Role - Bypassing RLS for admin operations