Skip to content

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

mermaid
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:#fff

Step-by-Step Flow

1. User Login

User provides credentials on login page:

typescript
// 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:

typescript
// src/contexts/AuthContext.tsx
const { data: { session } } = await supabase.auth.signInWithPassword({
  email,
  password,
});

Result: User session is created with:

  • user.id: User UUID
  • user.email: User email
  • session.access_token: JWT token

3. Fetch User Profile

After authentication, fetch user profile:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// Automatically handled by Supabase client
const supabase = createClient(url, key, {
  auth: {
    storage: localStorage,
    persistSession: true,
    autoRefreshToken: true,
  },
});

Session Refresh

Sessions are automatically refreshed:

typescript
// 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:

typescript
// 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 returned

How RLS Works

  1. JWT Token: Contains user.id (auth.uid())
  2. Policy Evaluation: PostgreSQL evaluates RLS policies using auth.uid()
  3. Automatic Filtering: Queries are filtered based on tenant_users table
  4. Transparent: Application code doesn't need explicit filtering

SSO Authentication Flow

For enterprise SSO (OIDC/SAML), the flow is similar but uses external provider:

mermaid
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 Dashboard

See Enterprise SSO Documentation for details.

Logout Flow

typescript
// src/contexts/AuthContext.tsx
const signOut = async () => {
  await supabase.auth.signOut();
  setUserRole(null);
  navigate('/login');
};

What Happens:

  1. Supabase session is cleared
  2. LocalStorage is cleared
  3. User is redirected to login
  4. Tenant context is lost

Route Protection

Protected routes check authentication and tenant:

typescript
// 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:

typescript
// Explicit tenant filtering (optional, RLS also does this)
const tenantId = await getCurrentUserTenantId();

const { data } = await supabase
  .from('shipments')
  .select('*')
  .eq('tenant_id', tenantId); // Explicit filter

Helper Functions

typescript
// 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

typescript
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

typescript
const { userRole } = useAuth();

if (!userRole) {
  // User is authenticated but has no role
  // Show message: "Role assignment pending"
  return <RoleAssignmentPending />;
}

Session Expired

typescript
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

typescript
// Verify session is valid
const { data: { session }, error } = await supabase.auth.getSession();

if (error || !session) {
  // Session invalid, redirect to login
  navigate('/login');
}

Best Practices

  1. Always Check Authentication: Verify user is authenticated before operations
  2. Verify Tenant Context: Ensure tenant_id is set before data operations
  3. Handle Errors Gracefully: Provide clear messages for auth failures
  4. Auto-Refresh: Let Supabase handle token refresh automatically
  5. Logout on Errors: Clear session on critical errors

Released under Commercial License