Overview
SkillRise uses Clerk for authentication, providing secure user management with OAuth providers, magic links, and email/password authentication. The system implements role-based access control (RBAC) with three distinct user roles.
User Roles
SkillRise implements three role levels through Clerk’s session claims:
Student Default role for all new users. Access to courses, quizzes, community, and analytics.
Educator Create and manage courses, view enrollment data, and access quiz insights.
Admin Full platform access including educator application review and system management.
Clerk Integration
User Model Schema
The User model syncs with Clerk through webhooks:
import mongoose from 'mongoose'
const userSchema = new mongoose . Schema (
{
_id: { type: String , required: true }, // Clerk user ID
name: { type: String , required: true },
email: { type: String , required: true },
imageUrl: { type: String , required: true },
enrolledCourses: [{
type: mongoose . Schema . Types . ObjectId ,
ref: 'Course' ,
}],
},
{ timestamps: true }
)
const User = mongoose . model ( 'User' , userSchema )
export default User
The _id field uses Clerk’s user ID as the primary key, ensuring seamless synchronization between Clerk and the application database.
Webhook Handlers
Clerk Webhook Events
SkillRise listens to three Clerk webhook events to keep user data synchronized:
server/controllers/webhooks.js
import { Webhook } from 'svix'
import User from '../models/User.js'
export const clerkWebhooks = async ( req , res ) => {
try {
const webhook = new Webhook ( process . env . CLERK_WEBHOOK_SECRET )
// Verify webhook signature
await webhook . verify ( JSON . stringify ( req . body ), {
'svix-id' : req . headers [ 'svix-id' ],
'svix-timestamp' : req . headers [ 'svix-timestamp' ],
'svix-signature' : req . headers [ 'svix-signature' ],
})
const { data , type } = req . body
switch ( type ) {
case 'user.created' : {
const userData = {
_id: data . id ,
email: data . email_addresses [ 0 ]. email_address ,
name: data . first_name + ' ' + data . last_name ,
imageUrl: data . image_url ,
}
await User . create ( userData )
res . json ({})
break
}
case 'user.updated' : {
const userData = {
email: data . email_addresses [ 0 ]. email_address ,
name: data . first_name + ' ' + data . last_name ,
imageUrl: data . image_url ,
}
await User . findByIdAndUpdate ( data . id , userData )
res . json ({})
break
}
case 'user.deleted' : {
await User . findByIdAndDelete ( data . id )
res . json ({})
break
}
default :
res . json ({})
}
} catch ( error ) {
console . error ( error )
res . status ( 500 ). json ({
success: false ,
message: 'An unexpected error occurred'
})
}
}
Signature Verification
Svix library verifies the webhook signature using headers to ensure authenticity.
Event Handling
Switch statement handles create, update, and delete events from Clerk.
Database Sync
User data is immediately synchronized with the MongoDB database.
Role-Based Middleware
Educator Protection
Middleware to protect educator-only routes:
server/middlewares/authMiddleware.js
export const protectEducator = ( req , res , next ) => {
try {
if ( ! req . auth ?. userId ) {
return res . status ( 401 ). json ({
success: false ,
message: 'Unauthorized Access'
})
}
const role = req . auth . sessionClaims ?. metadata ?. role
if ( role !== 'educator' ) {
return res . status ( 403 ). json ({
success: false ,
message: 'Unauthorized Access'
})
}
next ()
} catch ( error ) {
res . status ( 500 ). json ({
success: false ,
message: 'Internal Server Error'
})
}
}
Admin Protection
Middleware to protect admin-only routes:
server/middlewares/authMiddleware.js
export const protectAdmin = ( req , res , next ) => {
try {
if ( ! req . auth ?. userId ) {
return res . status ( 401 ). json ({
success: false ,
message: 'Unauthorized Access'
})
}
const role = req . auth . sessionClaims ?. metadata ?. role
if ( role !== 'admin' ) {
return res . status ( 403 ). json ({
success: false ,
message: 'Unauthorized Access'
})
}
next ()
} catch ( error ) {
res . status ( 500 ). json ({
success: false ,
message: 'Internal Server Error'
})
}
}
Role information is stored in Clerk’s session claims under metadata.role, allowing for flexible role assignment without database queries.
Environment Variables
Required environment variables for authentication:
CLERK_WEBHOOK_SECRET = whsec_xxxxxxxxxxxxx
CLERK_PUBLISHABLE_KEY = pk_test_xxxxxxxxxxxxx
CLERK_SECRET_KEY = sk_test_xxxxxxxxxxxxx
Security Features
Webhook Signature Verification
All Clerk webhooks are verified using Svix signatures to prevent unauthorized requests and ensure data integrity.
Session-Based Authorization
User roles are checked on every protected request through session claims, ensuring real-time access control.
Automatic Synchronization
User data is automatically synchronized between Clerk and the database, eliminating manual updates and reducing inconsistencies.
Using Clerk’s user ID as the primary key prevents ID conflicts and simplifies relationship management across the system.
Authentication Flow
Best Practices
Always Verify Signatures Never process webhook events without signature verification to prevent security vulnerabilities.
Use Session Claims Store role information in session claims for real-time authorization without database queries.
Idempotent Operations Design webhook handlers to handle duplicate events gracefully.
Error Logging Log all authentication errors for debugging and security monitoring.
Next Steps