Overview
EaseLMS uses Supabase Auth for authentication with custom middleware for session management and role-based access control. The system implements three-tier authentication: server-side, middleware, and client-side.
Architecture
┌─────────────────────────────────────────────────┐
│ Authentication Flow │
├─────────────────────────────────────────────────┤
│ │
│ 1. User Login → Supabase Auth │
│ 2. Session Created → Cookie Storage │
│ 3. Middleware → Session Validation │
│ 4. RLS Check → Database Access Control │
│ 5. Response → Protected Resource │
│ │
└─────────────────────────────────────────────────┘
Supabase Client Types
EaseLMS implements three types of Supabase clients for different contexts:
1. Server Client
Used in Server Components and API Routes with cookie-based session management:
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient () {
const supabaseUrl = process . env . NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY
if ( ! supabaseUrl || ! supabaseAnonKey ) {
throw new Error ( 'Missing Supabase environment variables' )
}
const cookieStore = await cookies ()
return createServerClient ( supabaseUrl , supabaseAnonKey , {
cookies: {
getAll () {
return cookieStore . getAll ()
},
setAll ( cookiesToSet ) {
try {
cookiesToSet . forEach (({ name , value , options }) =>
cookieStore . set ( name , value , options )
)
} catch {
// Server Component limitation - middleware handles refresh
}
},
},
})
}
The server client automatically reads session cookies and validates authentication state without requiring manual token management.
2. Service Role Client
Used for admin operations that bypass Row Level Security:
import { createClient as createServiceClient } from '@supabase/supabase-js'
export function createServiceRoleClient () {
const supabaseUrl = process . env . NEXT_PUBLIC_SUPABASE_URL
const supabaseServiceKey = process . env . SUPABASE_SERVICE_ROLE_KEY
if ( ! supabaseUrl || ! supabaseServiceKey ) {
throw new Error ( 'Missing Supabase service role key' )
}
return createServiceClient ( supabaseUrl , supabaseServiceKey , {
auth: {
autoRefreshToken: false ,
persistSession: false ,
},
})
}
The service role client bypasses all RLS policies. Use only for trusted server-side operations like user management and admin tasks.
3. Browser Client
Used in Client Components with automatic session handling:
import { createBrowserClient } from '@supabase/ssr'
import { checkSupabaseEnv } from './env-check'
export function createClient () {
const envCheck = checkSupabaseEnv ()
if ( ! envCheck . valid ) {
if ( process . env . NODE_ENV === 'development' ) {
console . error ( envCheck . message )
}
// Fallback to placeholder to prevent crashes
return createBrowserClient (
'https://placeholder.supabase.co' ,
'placeholder-key'
)
}
const supabaseUrl = process . env . NEXT_PUBLIC_SUPABASE_URL !
const supabaseAnonKey = process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY !
return createBrowserClient ( supabaseUrl , supabaseAnonKey )
}
Middleware Implementation
The middleware handles session validation and route protection:
lib/supabase/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse , type NextRequest } from 'next/server'
export async function updateSession ( request : NextRequest ) {
const supabaseUrl = process . env . NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY
if ( ! supabaseUrl || ! supabaseAnonKey ) {
return NextResponse . next ({ request })
}
let supabaseResponse = NextResponse . next ({ request })
const supabase = createServerClient ( supabaseUrl , supabaseAnonKey , {
cookies: {
getAll () {
return request . cookies . getAll ()
},
setAll ( cookiesToSet ) {
cookiesToSet . forEach (({ name , value }) =>
request . cookies . set ( name , value )
)
supabaseResponse = NextResponse . next ({ request })
cookiesToSet . forEach (({ name , value , options }) =>
supabaseResponse . cookies . set ( name , value , options )
)
},
},
})
// Get authenticated user
const { data : { user } } = await supabase . auth . getUser ()
// Public paths that don't require authentication
const publicPaths = [ '/auth' , '/forgot-password' ]
const isPublicPath = publicPaths . some ( path =>
request . nextUrl . pathname . startsWith ( path )
)
// Redirect unauthenticated users
if ( ! user && ! isPublicPath && ! request . nextUrl . pathname . startsWith ( '/api' )) {
const url = request . nextUrl . clone ()
url . pathname = request . nextUrl . pathname . startsWith ( '/admin' )
? '/auth/admin/login'
: '/auth/learner/login'
return NextResponse . redirect ( url )
}
return supabaseResponse
}
User Roles & Authorization
EaseLMS implements role-based access control with three user types:
User Types
Permissions:
Full system access
User management
Course creation and management
Payment and enrollment management
Platform settings configuration
Analytics access
Permissions:
Create and manage own courses
View student enrollments in their courses
Access course analytics
Manage course content
Permissions:
Enroll in courses
Access enrolled course content
Track progress
Download certificates
View personal dashboard
Role Check in Middleware
// Get user type from profile
const { data : profile } = await supabase
. from ( 'profiles' )
. select ( 'user_type' )
. eq ( 'id' , user . id )
. single ()
const userType = profile ?. user_type || 'user'
// Protect admin routes
if ( request . nextUrl . pathname . startsWith ( '/admin' ) && userType !== 'admin' ) {
url . pathname = '/auth/admin/login'
return NextResponse . redirect ( url )
}
Row Level Security (RLS)
Supabase RLS policies enforce authorization at the database level:
Example: Courses Table
-- Admins can do anything
CREATE POLICY "Admins have full access to courses"
ON courses
FOR ALL
USING ( auth . uid () IN (
SELECT id FROM profiles WHERE user_type = 'admin'
));
-- Instructors can manage their courses
CREATE POLICY "Instructors can manage their courses"
ON courses
FOR ALL
USING ( auth . uid () IN (
SELECT user_id FROM course_instructors WHERE course_id = courses . id
));
-- Public can view published courses
CREATE POLICY "Anyone can view published courses"
ON courses
FOR SELECT
USING (published = true);
Example: Enrollments Table
-- Users can view their own enrollments
CREATE POLICY "Users can view their own enrollments"
ON enrollments
FOR SELECT
USING ( auth . uid () = user_id);
-- Admins can view all enrollments
CREATE POLICY "Admins can view all enrollments"
ON enrollments
FOR SELECT
USING ( auth . uid () IN (
SELECT id FROM profiles WHERE user_type = 'admin'
));
Authentication Flows
Login Flow
import { createClient } from '@/lib/supabase/client'
async function handleLogin ( email : string , password : string ) {
const supabase = createClient ()
const { data , error } = await supabase . auth . signInWithPassword ({
email ,
password ,
})
if ( error ) {
throw new Error ( error . message )
}
// Session is automatically stored in cookies
// Middleware will handle redirection
return data
}
Signup Flow
async function handleSignup ( email : string , password : string , userData : any ) {
const supabase = createClient ()
// Create auth user
const { data : authData , error : authError } = await supabase . auth . signUp ({
email ,
password ,
})
if ( authError ) throw authError
// Create profile (triggered by database trigger or manual insert)
const { error : profileError } = await supabase
. from ( 'profiles' )
. insert ({
id: authData . user ! . id ,
email ,
full_name: userData . full_name ,
user_type: 'user' , // Default to learner
})
if ( profileError ) throw profileError
return authData
}
Logout Flow
async function handleLogout () {
const supabase = createClient ()
const { error } = await supabase . auth . signOut ()
if ( error ) throw error
// Middleware will redirect to login
window . location . href = '/auth/learner/login'
}
Password Reset Flow
async function requestPasswordReset ( email : string ) {
const supabase = createClient ()
const { error } = await supabase . auth . resetPasswordForEmail ( email , {
redirectTo: ` ${ window . location . origin } /auth/reset-password` ,
})
if ( error ) throw error
}
async function updatePassword ( newPassword : string ) {
const supabase = createClient ()
const { error } = await supabase . auth . updateUser ({
password: newPassword ,
})
if ( error ) throw error
}
Session Management
Auto-Refresh
Supabase Auth automatically refreshes sessions before expiry:
const supabase = createClient ()
// Listen for auth state changes
supabase . auth . onAuthStateChange (( event , session ) => {
if ( event === 'SIGNED_OUT' ) {
// Clear local data
} else if ( event === 'TOKEN_REFRESHED' ) {
// Session refreshed automatically
}
})
Manual Session Check
async function getSession () {
const supabase = createClient ()
const { data : { session } } = await supabase . auth . getSession ()
return session
}
async function getUser () {
const supabase = createClient ()
const { data : { user } } = await supabase . auth . getUser ()
return user
}
Security Best Practices
Never commit credentials to version control: # Public (safe for client-side)
NEXT_PUBLIC_SUPABASE_URL = https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY = eyJ...
# Private (server-side only)
SUPABASE_SERVICE_ROLE_KEY = eyJ...
Only use service role client for:
User management operations
Admin-only database operations
Bypassing RLS when necessary
Never expose service role key to client-side code.
Always enable RLS on all tables and create policies for:
Read access (SELECT)
Write access (INSERT, UPDATE, DELETE)
Admin override
Supabase automatically sets secure cookies:
HttpOnly flag prevents XSS attacks
Secure flag ensures HTTPS-only
SameSite prevents CSRF attacks
Debugging Authentication
Enable Debug Logging
if ( process . env . NODE_ENV === 'development' ) {
console . log ( 'Middleware user type check:' , {
userId: user . id ,
userType ,
profileExists: !! profile ,
pathname: request . nextUrl . pathname ,
})
}
Common Issues
Session Not Persisting Check that cookies are enabled and HTTPS is used in production.
RLS Blocking Access Verify RLS policies match your user’s role and use service role client for admin operations.
Redirect Loop Ensure middleware doesn’t redirect authenticated users accessing valid routes.
Missing User Profile Verify profile is created on signup (use database trigger or manual insert).
Next Steps