Low-Level Authentication Flow Design for Student, Trainer, and Consultant Services Using Keycloak

Yes — the clean low-level flow should use Keycloak as the central Identity Provider, and Student / Trainer / Consultant services should never handle passwords directly.

Recommended login pattern:

Browser → Student/Trainer App → Keycloak → Student/Trainer App → App Database

Use Authorization Code Flow with PKCE.


1. High-Level Login Flow

flowchart TD
    A[Browser / User] --> B[Student Portal]
    A --> C[Trainer Portal]

    B --> D[Keycloak Login Page]
    C --> D[Keycloak Login Page]

    D --> E[User enters username/password]
    E --> F[Keycloak validates credentials]

    F --> G{Login Success?}
    G -->|No| D
    G -->|Yes| H[Keycloak creates SSO session]

    H --> I[Redirect back to Student/Trainer App with authorization code]

    I --> J[App exchanges code for tokens]
    J --> K[Keycloak returns ID Token, Access Token, Refresh Token]

    K --> L[App reads user identity from token]
    L --> M[App checks local DB user mapping]

    M --> N{Role allowed?}
    N -->|Student role| O[Student Dashboard]
    N -->|Trainer role| P[Trainer Dashboard]
    N -->|No role| Q[Access Denied]

2. Detailed Student Login Flow

sequenceDiagram
    autonumber

    participant User as User / Browser
    participant StudentFE as Student Frontend
    participant StudentBE as Student Backend
    participant KC as Keycloak
    participant DB as Application DB

    User->>StudentFE: Open student portal
    StudentFE->>StudentBE: GET /student/dashboard

    StudentBE->>StudentBE: Check local app session

    alt No active session
        StudentBE-->>User: Redirect to Keycloak /authorize

        User->>KC: GET /realms/{realm}/protocol/openid-connect/auth
        Note over User,KC: Sends client_id, redirect_uri, scope, state, nonce, code_challenge

        KC-->>User: Show login page
        User->>KC: Submit username/password

        KC->>KC: Validate credentials
        KC->>KC: Create Keycloak SSO session cookie

        KC-->>User: Redirect to Student callback with authorization_code
        User->>StudentBE: GET /auth/callback?code=AUTH_CODE&state=STATE

        StudentBE->>StudentBE: Validate state

        StudentBE->>KC: POST /token
        Note over StudentBE,KC: Sends code, client_id, redirect_uri, code_verifier

        KC-->>StudentBE: Return ID Token, Access Token, Refresh Token

        StudentBE->>StudentBE: Validate ID token signature and claims
        StudentBE->>StudentBE: Extract sub, email, name, roles

        StudentBE->>DB: Find user by keycloak_user_id = token.sub

        alt User does not exist locally
            StudentBE->>DB: Create users row
            StudentBE->>DB: Create or check student role/profile
        else User exists
            StudentBE->>DB: Update last_login_at, email_verified, profile data if needed
        end

        StudentBE->>DB: Check user_service_roles contains student

        alt Has student access
            StudentBE->>StudentBE: Create local app session
            StudentBE-->>User: Set secure HttpOnly session cookie
            StudentBE-->>User: Redirect to /student/dashboard
        else No student role
            StudentBE-->>User: 403 Access Denied
        end

    else Active session exists
        StudentBE->>DB: Load student profile
        StudentBE-->>StudentFE: Return student dashboard
    end

3. Detailed Trainer Login Flow

Trainer login is almost the same, but the app checks for the trainer role instead of the student role.

sequenceDiagram
    autonumber

    participant User as User / Browser
    participant TrainerFE as Trainer Frontend
    participant TrainerBE as Trainer Backend
    participant KC as Keycloak
    participant DB as Application DB

    User->>TrainerFE: Open trainer portal
    TrainerFE->>TrainerBE: GET /trainer/dashboard

    TrainerBE->>TrainerBE: Check local app session

    alt No active session
        TrainerBE-->>User: Redirect to Keycloak authorization endpoint

        User->>KC: Authorization request
        Note over User,KC: client_id=trainer-app, redirect_uri, scope, state, nonce, code_challenge

        alt User already logged in to Keycloak from Student portal
            KC->>KC: Detect existing Keycloak SSO session
            KC-->>User: Redirect back with authorization_code
        else User not logged in
            KC-->>User: Show login page
            User->>KC: Submit credentials
            KC->>KC: Validate credentials
            KC->>KC: Create SSO session
            KC-->>User: Redirect back with authorization_code
        end

        User->>TrainerBE: GET /auth/callback?code=AUTH_CODE&state=STATE

        TrainerBE->>KC: Exchange authorization code for tokens
        KC-->>TrainerBE: ID Token, Access Token, Refresh Token

        TrainerBE->>TrainerBE: Validate token
        TrainerBE->>TrainerBE: Extract token.sub and roles

        TrainerBE->>DB: Find users.keycloak_user_id = token.sub
        TrainerBE->>DB: Check user_service_roles contains trainer
        TrainerBE->>DB: Check trainer_profiles.approval_status

        alt User has trainer access and approved
            TrainerBE->>TrainerBE: Create trainer app session
            TrainerBE-->>User: Set secure HttpOnly session cookie
            TrainerBE-->>User: Redirect to trainer dashboard
        else User is not trainer
            TrainerBE-->>User: 403 Not allowed for trainer portal
        else Trainer pending approval
            TrainerBE-->>User: Show pending approval page
        end

    else Active trainer session exists
        TrainerBE->>DB: Load trainer profile
        TrainerBE-->>TrainerFE: Return trainer dashboard
    end

4. What Data Passes Between Each System?

FromToData PassedPurpose
BrowserStudent/Trainer AppRequest URL, cookiesUser opens dashboard
AppBrowserRedirect to KeycloakStart login
BrowserKeycloakclient_id, redirect_uri, scope, state, nonce, code_challengeStart OIDC login
UserKeycloakUsername/password/OTPAuthentication
KeycloakBrowserAuthorization codeTemporary login code
BrowserApp callback URLcode, stateReturn login result
App BackendKeycloakcode, client_id, code_verifierExchange code for tokens
KeycloakApp Backendid_token, access_token, refresh_tokenAuthenticated user data
App BackendApp DBkeycloak_user_id, email, rolesMap Keycloak user to local app user
App BackendBrowserSecure session cookieMaintain app login session

5. Token Exchange Flow

flowchart LR
    A[Browser] -->|1. Open Student App| B[Student Backend]

    B -->|2. Redirect to /authorize| C[Keycloak]

    C -->|3. Login Page| A

    A -->|4. Username Password OTP| C

    C -->|5. Redirect with auth code| B

    B -->|6. Exchange code using token endpoint| C

    C -->|7. Return tokens| B

    B -->|8. Validate token and create app session| D[Application DB]

    B -->|9. Set HttpOnly cookie| A

6. Example Authorization Request to Keycloak

When user opens Student portal:

GET https://auth.example.com/realms/myrealm/protocol/openid-connect/auth
    ?client_id=student-app
    &redirect_uri=https://student.example.com/auth/callback
    &response_type=code
    &scope=openid profile email
    &state=random_csrf_value
    &nonce=random_nonce_value
    &code_challenge=generated_pkce_challenge
    &code_challenge_method=S256

For Trainer portal:

GET https://auth.example.com/realms/myrealm/protocol/openid-connect/auth
    ?client_id=trainer-app
    &redirect_uri=https://trainer.example.com/auth/callback
    &response_type=code
    &scope=openid profile email
    &state=random_csrf_value
    &nonce=random_nonce_value
    &code_challenge=generated_pkce_challenge
    &code_challenge_method=S256

7. Example Token Response from Keycloak

{
  "access_token": "eyJhbGciOi...",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOi...",
  "scope": "openid profile email"
}

8. Important Claims Inside Token

Your backend mainly needs these fields:

{
  "sub": "8d5f9b18-16c2-49f3-90e2-123456789abc",
  "email": "user@example.com",
  "email_verified": true,
  "preferred_username": "rajesh",
  "given_name": "Rajesh",
  "family_name": "Kumar",
  "realm_access": {
    "roles": [
      "student",
      "trainer"
    ]
  }
}

Application DB mapping:

token.sub  → users.keycloak_user_id
email      → users.email
roles      → user_service_roles

9. Student to Trainer SSO Flow

If the user already logged in as Student and then opens Trainer portal, they should not enter password again.

sequenceDiagram
    autonumber

    participant User as Browser
    participant Student as Student App
    participant Trainer as Trainer App
    participant KC as Keycloak
    participant DB as App DB

    User->>Student: Login to student portal
    Student->>KC: Redirect user to Keycloak
    KC->>User: Login page
    User->>KC: Submit credentials
    KC->>KC: Create Keycloak SSO session
    KC-->>Student: Return authorization code
    Student->>KC: Exchange code for tokens
    KC-->>Student: Tokens
    Student->>DB: Check student role
    Student-->>User: Student dashboard

    User->>Trainer: Open trainer portal
    Trainer->>KC: Redirect to Keycloak authorize endpoint

    KC->>KC: Existing SSO session found
    KC-->>Trainer: Return authorization code without asking password

    Trainer->>KC: Exchange code for tokens
    KC-->>Trainer: Tokens

    Trainer->>DB: Check trainer role/profile

    alt User has trainer role
        Trainer-->>User: Trainer dashboard
    else User does not have trainer role
        Trainer-->>User: Access denied or apply as trainer
    end

10. Access Control Decision Flow

flowchart TD
    A[User logged in via Keycloak] --> B[App receives token]
    B --> C[Extract keycloak_user_id from token.sub]
    C --> D[Find user in local DB]

    D --> E{User exists?}
    E -->|No| F[Create local user profile]
    E -->|Yes| G[Load existing user]

    F --> H[Check token roles]
    G --> H[Check token roles]

    H --> I{Requested service?}

    I -->|Student Portal| J{Has student role?}
    I -->|Trainer Portal| K{Has trainer role?}
    I -->|Consultant Portal| L{Has consultant role?}

    J -->|Yes| M[Allow student dashboard]
    J -->|No| N[Deny or show registration]

    K -->|Yes| O[Check trainer profile approval]
    K -->|No| P[Deny or apply as trainer]

    O -->|Approved| Q[Allow trainer dashboard]
    O -->|Pending| R[Show pending approval]

    L -->|Yes| S[Check consultant approval]
    L -->|No| T[Deny or apply as consultant]

11. Refresh Token Flow

When access token expires, the app should refresh it.

sequenceDiagram
    autonumber

    participant Browser as Browser
    participant App as Student/Trainer Backend
    participant KC as Keycloak
    participant DB as App DB

    Browser->>App: Request dashboard/API
    App->>App: Check app session and token expiry

    alt Access token still valid
        App->>DB: Load required data
        App-->>Browser: Return response
    else Access token expired
        App->>KC: POST /token with grant_type=refresh_token
        KC-->>App: New access token and refresh token

        alt Refresh success
            App->>App: Update session token data
            App->>DB: Load required data
            App-->>Browser: Return response
        else Refresh failed
            App-->>Browser: Redirect to Keycloak login
        end
    end

12. Logout Flow

sequenceDiagram
    autonumber

    participant User as Browser
    participant App as Student/Trainer App
    participant KC as Keycloak

    User->>App: Click logout

    App->>App: Destroy local app session
    App-->>User: Clear app session cookie

    App-->>User: Redirect to Keycloak logout endpoint

    User->>KC: GET /logout?id_token_hint=ID_TOKEN&post_logout_redirect_uri=APP_URL

    KC->>KC: Destroy Keycloak SSO session
    KC-->>User: Redirect back to public homepage

13. Recommended Keycloak Clients

ServiceKeycloak Client IDTypeRedirect URI
Student Portalstudent-appConfidential or Public with PKCEhttps://student.example.com/auth/callback
Trainer Portaltrainer-appConfidential or Public with PKCEhttps://trainer.example.com/auth/callback
Consultant Portalconsultant-appConfidential or Public with PKCEhttps://consultant.example.com/auth/callback
Admin Portaladmin-appConfidentialhttps://admin.example.com/auth/callback

For backend-based apps, use confidential clients.

For pure SPA/mobile apps, use public clients with PKCE.


14. Recommended Local Tables Used During Login

users
user_service_roles
student_profiles
trainer_profiles
consultant_profiles
audit_logs

Main lookup:

SELECT *
FROM users
WHERE keycloak_user_id = '<token.sub>';

Check student role:

SELECT *
FROM user_service_roles
WHERE user_id = 10
AND service_type = 'student'
AND status = 'active';

Check trainer role:

SELECT *
FROM user_service_roles
WHERE user_id = 10
AND service_type = 'trainer'
AND status = 'active';

Check trainer approval:

SELECT *
FROM trainer_profiles
WHERE user_id = 10
AND approval_status = 'approved'
AND status = 'active';

15. Best Final Design

flowchart TD
    Browser[Browser] --> StudentApp[Student App]
    Browser --> TrainerApp[Trainer App]
    Browser --> ConsultantApp[Consultant App]

    StudentApp --> Keycloak[Keycloak]
    TrainerApp --> Keycloak
    ConsultantApp --> Keycloak

    Keycloak --> StudentApp
    Keycloak --> TrainerApp
    Keycloak --> ConsultantApp

    StudentApp --> AppDB[(Application Database)]
    TrainerApp --> AppDB
    ConsultantApp --> AppDB

    AppDB --> Users[users table]
    AppDB --> Roles[user_service_roles table]
    AppDB --> StudentProfile[student_profiles table]
    AppDB --> TrainerProfile[trainer_profiles table]
    AppDB --> ConsultantProfile[consultant_profiles table]

The golden rule:

Keycloak decides: Who are you?
Application DB decides: What can you do inside Student/Trainer/Consultant service?

Below is a low-level database schema for your platform with 3 services:

  1. Student Service
  2. Trainer Service
  3. Consultant Service
  4. Authentication handled by Keycloak

Important: Do not store passwords in your application database. Keycloak will manage login, password, MFA, social login, sessions, roles, etc. Your app should only store the Keycloak user reference.


Recommended Architecture

Keycloak
  |
  | JWT Token
  |
Application Backend
  |
  | keycloak_user_id / sub
  |
Application Database
  |
  |-- common user profile
  |-- student profile
  |-- trainer profile
  |-- consultant profile
  |-- courses
  |-- batches
  |-- enrollments
  |-- bookings
  |-- consulting requests
  |-- payments

1. Common User Tables

Table: users

This table maps your application user with Keycloak user.

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    keycloak_user_id VARCHAR(100) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    username VARCHAR(150) NULL,

    first_name VARCHAR(100) NULL,
    last_name VARCHAR(100) NULL,
    phone VARCHAR(30) NULL,
    profile_image VARCHAR(500) NULL,

    status ENUM('active', 'inactive', 'blocked', 'deleted') DEFAULT 'active',

    email_verified BOOLEAN DEFAULT FALSE,
    phone_verified BOOLEAN DEFAULT FALSE,

    last_login_at DATETIME NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_users_keycloak_user_id (keycloak_user_id),
    INDEX idx_users_email (email),
    INDEX idx_users_status (status)
);

Table: user_service_roles

One user can be a student, trainer, consultant, or all three.

CREATE TABLE user_service_roles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL,
    service_type ENUM('student', 'trainer', 'consultant') NOT NULL,

    status ENUM('active', 'inactive', 'pending', 'rejected') DEFAULT 'active',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uq_user_service (user_id, service_type),

    CONSTRAINT fk_user_service_roles_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

Example:

User Rajesh can have:
- student
- trainer
- consultant

So instead of creating separate login tables, keep one user identity and attach multiple service roles.


2. Student Service Tables

Table: student_profiles

CREATE TABLE student_profiles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL UNIQUE,

    student_code VARCHAR(50) UNIQUE NULL,
    education_level VARCHAR(100) NULL,
    current_company VARCHAR(150) NULL,
    job_title VARCHAR(150) NULL,

    country VARCHAR(100) NULL,
    city VARCHAR(100) NULL,
    timezone VARCHAR(100) NULL,

    learning_goal TEXT NULL,
    experience_level ENUM('beginner', 'intermediate', 'advanced') DEFAULT 'beginner',

    status ENUM('active', 'inactive', 'blocked') DEFAULT 'active',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_student_profiles_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

Table: courses

CREATE TABLE courses (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    title VARCHAR(255) NOT NULL,
    slug VARCHAR(255) NOT NULL UNIQUE,

    short_description VARCHAR(500) NULL,
    description LONGTEXT NULL,

    category VARCHAR(150) NULL,
    level ENUM('foundation', 'essential', 'intermediate', 'advanced') DEFAULT 'foundation',

    duration_hours INT DEFAULT 0,
    language VARCHAR(100) DEFAULT 'English',

    price DECIMAL(10,2) DEFAULT 0.00,
    currency VARCHAR(10) DEFAULT 'USD',

    status ENUM('draft', 'published', 'archived') DEFAULT 'draft',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_courses_slug (slug),
    INDEX idx_courses_status (status),
    INDEX idx_courses_category (category)
);

Table: course_modules

CREATE TABLE course_modules (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    course_id BIGINT NOT NULL,

    title VARCHAR(255) NOT NULL,
    description TEXT NULL,
    sort_order INT DEFAULT 0,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_course_modules_course
        FOREIGN KEY (course_id) REFERENCES courses(id)
        ON DELETE CASCADE
);

Table: lessons

CREATE TABLE lessons (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    course_id BIGINT NOT NULL,
    module_id BIGINT NULL,

    title VARCHAR(255) NOT NULL,
    content LONGTEXT NULL,

    lesson_type ENUM('video', 'text', 'quiz', 'assignment', 'live_session') DEFAULT 'text',

    video_url VARCHAR(500) NULL,
    duration_minutes INT DEFAULT 0,

    sort_order INT DEFAULT 0,
    is_free_preview BOOLEAN DEFAULT FALSE,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_lessons_course
        FOREIGN KEY (course_id) REFERENCES courses(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_lessons_module
        FOREIGN KEY (module_id) REFERENCES course_modules(id)
        ON DELETE SET NULL
);

Table: student_enrollments

CREATE TABLE student_enrollments (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    student_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,

    enrollment_status ENUM('enrolled', 'in_progress', 'completed', 'cancelled') DEFAULT 'enrolled',

    enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    completed_at DATETIME NULL,

    progress_percentage DECIMAL(5,2) DEFAULT 0.00,

    certificate_issued BOOLEAN DEFAULT FALSE,
    certificate_url VARCHAR(500) NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uq_student_course (student_id, course_id),

    CONSTRAINT fk_student_enrollments_student
        FOREIGN KEY (student_id) REFERENCES student_profiles(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_student_enrollments_course
        FOREIGN KEY (course_id) REFERENCES courses(id)
        ON DELETE CASCADE
);

Table: student_lesson_progress

CREATE TABLE student_lesson_progress (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    student_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,
    lesson_id BIGINT NOT NULL,

    status ENUM('not_started', 'in_progress', 'completed') DEFAULT 'not_started',

    started_at DATETIME NULL,
    completed_at DATETIME NULL,

    watched_seconds INT DEFAULT 0,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uq_student_lesson (student_id, lesson_id),

    CONSTRAINT fk_progress_student
        FOREIGN KEY (student_id) REFERENCES student_profiles(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_progress_course
        FOREIGN KEY (course_id) REFERENCES courses(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_progress_lesson
        FOREIGN KEY (lesson_id) REFERENCES lessons(id)
        ON DELETE CASCADE
);

3. Trainer Service Tables

Table: trainer_profiles

CREATE TABLE trainer_profiles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL UNIQUE,

    trainer_code VARCHAR(50) UNIQUE NULL,

    headline VARCHAR(255) NULL,
    bio TEXT NULL,

    total_experience_years DECIMAL(4,1) DEFAULT 0.0,
    primary_skill VARCHAR(150) NULL,

    linkedin_url VARCHAR(500) NULL,
    website_url VARCHAR(500) NULL,

    hourly_rate DECIMAL(10,2) DEFAULT 0.00,
    currency VARCHAR(10) DEFAULT 'USD',

    approval_status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
    status ENUM('active', 'inactive', 'blocked') DEFAULT 'active',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_trainer_profiles_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

Table: trainer_skills

CREATE TABLE trainer_skills (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    trainer_id BIGINT NOT NULL,

    skill_name VARCHAR(150) NOT NULL,
    experience_years DECIMAL(4,1) DEFAULT 0.0,

    skill_level ENUM('beginner', 'intermediate', 'advanced', 'expert') DEFAULT 'intermediate',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_trainer_skills_trainer
        FOREIGN KEY (trainer_id) REFERENCES trainer_profiles(id)
        ON DELETE CASCADE,

    INDEX idx_trainer_skills_skill_name (skill_name)
);

Table: trainer_courses

This connects trainers with courses.

CREATE TABLE trainer_courses (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    trainer_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,

    role ENUM('primary', 'assistant', 'guest') DEFAULT 'primary',

    assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY uq_trainer_course (trainer_id, course_id),

    CONSTRAINT fk_trainer_courses_trainer
        FOREIGN KEY (trainer_id) REFERENCES trainer_profiles(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_trainer_courses_course
        FOREIGN KEY (course_id) REFERENCES courses(id)
        ON DELETE CASCADE
);

Table: training_batches

CREATE TABLE training_batches (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    course_id BIGINT NOT NULL,
    trainer_id BIGINT NOT NULL,

    batch_name VARCHAR(255) NOT NULL,

    start_date DATE NOT NULL,
    end_date DATE NULL,

    start_time TIME NULL,
    end_time TIME NULL,

    timezone VARCHAR(100) DEFAULT 'UTC',

    mode ENUM('online', 'offline', 'hybrid') DEFAULT 'online',

    meeting_url VARCHAR(500) NULL,
    location VARCHAR(500) NULL,

    max_students INT DEFAULT 0,

    status ENUM('scheduled', 'ongoing', 'completed', 'cancelled') DEFAULT 'scheduled',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_training_batches_course
        FOREIGN KEY (course_id) REFERENCES courses(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_training_batches_trainer
        FOREIGN KEY (trainer_id) REFERENCES trainer_profiles(id)
        ON DELETE CASCADE
);

Table: batch_students

CREATE TABLE batch_students (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    batch_id BIGINT NOT NULL,
    student_id BIGINT NOT NULL,

    joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    status ENUM('active', 'completed', 'dropped') DEFAULT 'active',

    UNIQUE KEY uq_batch_student (batch_id, student_id),

    CONSTRAINT fk_batch_students_batch
        FOREIGN KEY (batch_id) REFERENCES training_batches(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_batch_students_student
        FOREIGN KEY (student_id) REFERENCES student_profiles(id)
        ON DELETE CASCADE
);

Table: batch_sessions

CREATE TABLE batch_sessions (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    batch_id BIGINT NOT NULL,

    session_title VARCHAR(255) NOT NULL,
    session_description TEXT NULL,

    session_date DATE NOT NULL,
    start_time TIME NOT NULL,
    end_time TIME NOT NULL,

    meeting_url VARCHAR(500) NULL,

    status ENUM('scheduled', 'completed', 'cancelled') DEFAULT 'scheduled',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_batch_sessions_batch
        FOREIGN KEY (batch_id) REFERENCES training_batches(id)
        ON DELETE CASCADE
);

Table: student_attendance

CREATE TABLE student_attendance (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    session_id BIGINT NOT NULL,
    student_id BIGINT NOT NULL,

    attendance_status ENUM('present', 'absent', 'late') DEFAULT 'absent',

    joined_at DATETIME NULL,
    left_at DATETIME NULL,

    remarks TEXT NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY uq_session_student_attendance (session_id, student_id),

    CONSTRAINT fk_attendance_session
        FOREIGN KEY (session_id) REFERENCES batch_sessions(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_attendance_student
        FOREIGN KEY (student_id) REFERENCES student_profiles(id)
        ON DELETE CASCADE
);

4. Consultant Service Tables

Table: consultant_profiles

CREATE TABLE consultant_profiles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL UNIQUE,

    consultant_code VARCHAR(50) UNIQUE NULL,

    headline VARCHAR(255) NULL,
    bio TEXT NULL,

    total_experience_years DECIMAL(4,1) DEFAULT 0.0,

    primary_domain VARCHAR(150) NULL,

    hourly_rate DECIMAL(10,2) DEFAULT 0.00,
    daily_rate DECIMAL(10,2) DEFAULT 0.00,
    currency VARCHAR(10) DEFAULT 'USD',

    linkedin_url VARCHAR(500) NULL,
    portfolio_url VARCHAR(500) NULL,

    approval_status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
    status ENUM('active', 'inactive', 'blocked') DEFAULT 'active',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_consultant_profiles_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

Table: consultant_skills

CREATE TABLE consultant_skills (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    consultant_id BIGINT NOT NULL,

    skill_name VARCHAR(150) NOT NULL,
    experience_years DECIMAL(4,1) DEFAULT 0.0,

    skill_level ENUM('beginner', 'intermediate', 'advanced', 'expert') DEFAULT 'expert',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_consultant_skills_consultant
        FOREIGN KEY (consultant_id) REFERENCES consultant_profiles(id)
        ON DELETE CASCADE,

    INDEX idx_consultant_skills_skill_name (skill_name)
);

Table: consulting_services

These are services offered by consultants.

CREATE TABLE consulting_services (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    consultant_id BIGINT NOT NULL,

    title VARCHAR(255) NOT NULL,
    slug VARCHAR(255) NOT NULL UNIQUE,

    description LONGTEXT NULL,

    service_category VARCHAR(150) NULL,

    pricing_type ENUM('hourly', 'fixed', 'daily', 'custom') DEFAULT 'hourly',

    price DECIMAL(10,2) DEFAULT 0.00,
    currency VARCHAR(10) DEFAULT 'USD',

    estimated_duration_hours INT DEFAULT 0,

    status ENUM('draft', 'published', 'archived') DEFAULT 'draft',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_consulting_services_consultant
        FOREIGN KEY (consultant_id) REFERENCES consultant_profiles(id)
        ON DELETE CASCADE
);

Table: consulting_requests

This is when a student/client requests consulting.

CREATE TABLE consulting_requests (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    requested_by_user_id BIGINT NOT NULL,
    consultant_id BIGINT NULL,
    consulting_service_id BIGINT NULL,

    title VARCHAR(255) NOT NULL,
    description LONGTEXT NULL,

    budget DECIMAL(10,2) DEFAULT 0.00,
    currency VARCHAR(10) DEFAULT 'USD',

    priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',

    request_status ENUM(
        'open',
        'assigned',
        'accepted',
        'in_progress',
        'completed',
        'cancelled',
        'rejected'
    ) DEFAULT 'open',

    expected_start_date DATE NULL,
    expected_end_date DATE NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_consulting_requests_user
        FOREIGN KEY (requested_by_user_id) REFERENCES users(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_consulting_requests_consultant
        FOREIGN KEY (consultant_id) REFERENCES consultant_profiles(id)
        ON DELETE SET NULL,

    CONSTRAINT fk_consulting_requests_service
        FOREIGN KEY (consulting_service_id) REFERENCES consulting_services(id)
        ON DELETE SET NULL
);

Table: consulting_bookings

CREATE TABLE consulting_bookings (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    consulting_request_id BIGINT NOT NULL,
    consultant_id BIGINT NOT NULL,
    booked_by_user_id BIGINT NOT NULL,

    booking_date DATE NOT NULL,
    start_time TIME NOT NULL,
    end_time TIME NOT NULL,

    timezone VARCHAR(100) DEFAULT 'UTC',

    meeting_url VARCHAR(500) NULL,

    booking_status ENUM(
        'pending',
        'confirmed',
        'completed',
        'cancelled',
        'rescheduled'
    ) DEFAULT 'pending',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_consulting_bookings_request
        FOREIGN KEY (consulting_request_id) REFERENCES consulting_requests(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_consulting_bookings_consultant
        FOREIGN KEY (consultant_id) REFERENCES consultant_profiles(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_consulting_bookings_user
        FOREIGN KEY (booked_by_user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

5. Payment Tables

Table: payments

Use this for course enrollment, trainer booking, consultant booking, etc.

CREATE TABLE payments (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL,

    payment_for ENUM('course', 'batch', 'consulting', 'subscription') NOT NULL,

    reference_id BIGINT NOT NULL,

    amount DECIMAL(10,2) NOT NULL,
    currency VARCHAR(10) DEFAULT 'USD',

    payment_gateway ENUM('paypal', 'stripe', 'razorpay', 'paytm', 'bank_transfer') NULL,

    gateway_transaction_id VARCHAR(255) NULL,

    payment_status ENUM(
        'pending',
        'paid',
        'failed',
        'refunded',
        'cancelled'
    ) DEFAULT 'pending',

    paid_at DATETIME NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_payments_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE,

    INDEX idx_payments_reference (payment_for, reference_id),
    INDEX idx_payments_status (payment_status)
);

Table: invoices

CREATE TABLE invoices (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    payment_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,

    invoice_number VARCHAR(100) NOT NULL UNIQUE,

    invoice_date DATE NOT NULL,

    subtotal DECIMAL(10,2) NOT NULL,
    tax_amount DECIMAL(10,2) DEFAULT 0.00,
    total_amount DECIMAL(10,2) NOT NULL,

    currency VARCHAR(10) DEFAULT 'USD',

    invoice_url VARCHAR(500) NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_invoices_payment
        FOREIGN KEY (payment_id) REFERENCES payments(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_invoices_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

6. Review and Rating Tables

Table: reviews

CREATE TABLE reviews (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    reviewer_user_id BIGINT NOT NULL,

    review_for ENUM('course', 'trainer', 'consultant', 'consulting_service') NOT NULL,
    reference_id BIGINT NOT NULL,

    rating INT NOT NULL,
    review_text TEXT NULL,

    status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_reviews_user
        FOREIGN KEY (reviewer_user_id) REFERENCES users(id)
        ON DELETE CASCADE,

    INDEX idx_reviews_reference (review_for, reference_id),
    INDEX idx_reviews_rating (rating)
);

7. Documents and Verification

Useful for trainer and consultant approval.

Table: user_documents

CREATE TABLE user_documents (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL,

    document_type ENUM(
        'resume',
        'certificate',
        'id_proof',
        'address_proof',
        'experience_letter',
        'other'
    ) NOT NULL,

    document_name VARCHAR(255) NULL,
    document_url VARCHAR(500) NOT NULL,

    verification_status ENUM('pending', 'verified', 'rejected') DEFAULT 'pending',

    remarks TEXT NULL,

    uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    verified_at DATETIME NULL,

    CONSTRAINT fk_user_documents_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

8. Notification Tables

Table: notifications

CREATE TABLE notifications (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NOT NULL,

    title VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,

    notification_type ENUM(
        'course',
        'batch',
        'payment',
        'consulting',
        'system'
    ) DEFAULT 'system',

    is_read BOOLEAN DEFAULT FALSE,

    read_at DATETIME NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_notifications_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE,

    INDEX idx_notifications_user_read (user_id, is_read)
);

9. Audit Log Table

Very useful for admin and security.

CREATE TABLE audit_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT NULL,

    action VARCHAR(255) NOT NULL,
    module VARCHAR(100) NOT NULL,

    ip_address VARCHAR(100) NULL,
    user_agent TEXT NULL,

    old_value JSON NULL,
    new_value JSON NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_audit_logs_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE SET NULL,

    INDEX idx_audit_logs_user (user_id),
    INDEX idx_audit_logs_module (module)
);

Keycloak Role Mapping

In Keycloak, create these roles:

student
trainer
consultant
admin
super_admin

Recommended Keycloak groups:

/students
/trainers
/consultants
/admins

Your JWT token may contain:

{
  "sub": "keycloak-user-id",
  "email": "user@example.com",
  "preferred_username": "rajesh",
  "realm_access": {
    "roles": ["student", "trainer"]
  }
}

Your application will use:

JWT sub = users.keycloak_user_id

Final Table List

ModuleTables
Commonusers, user_service_roles
Studentstudent_profiles, student_enrollments, student_lesson_progress
Trainertrainer_profiles, trainer_skills, trainer_courses, training_batches, batch_students, batch_sessions, student_attendance
Consultantconsultant_profiles, consultant_skills, consulting_services, consulting_requests, consulting_bookings
Coursecourses, course_modules, lessons
Paymentpayments, invoices
Reviewreviews
Documentuser_documents
Notificationnotifications
Securityaudit_logs

Best Practice

Use this structure:

Keycloak = Authentication + Authorization
Application DB = Business Data

Do not create separate login tables for student, trainer, and consultant. Keep one central users table and then create separate profile tables for each service. This keeps the system clean, scalable, and easy to maintain.

Related Posts

How to Use Two GitHub Accounts on One Mac with Different SSH Keys

Managing two GitHub accounts on the same Mac is very common. For example, you may have: The problem starts when both accounts use GitHub SSH URLs like…

Read More

Github: GitHub CLI gh — Install, Authenticate, and Use GitHub from the Terminal

1. What is gh? gh is the official GitHub command-line interface. It brings GitHub features such as repositories, pull requests, issues, GitHub Actions, Codespaces, releases, secrets, variables,…

Read More

Datadog: Application Error Tracking in EKS using Datadog, DogStatsD, APM, Logs, and Error Tracking

Master Guide: Application Error Tracking in EKS using Datadog, DogStatsD, APM, Logs, and Error Tracking First, tiny naming correction: it is DogStatsD, not DogStashD. DogStatsD is Datadog’s…

Read More

MongoDB – Complete End-to-End MongoDB Tutorial Blog: From Basics to Advanced

MongoDB is a NoSQL document database. Instead of storing data in rows and columns like a relational database, MongoDB stores data as documents inside collections. These documents…

Read More