Learner Map

Author: James Derick Billate
Reviewer: Krizha Onise Cortez
Creation Date: July 6, 2026
Status: Published
References: https://github.com/wyzlab/WyzQuests/issues/78

INTRODUCTION AND GOALS

Problem Summary: Allows the learner to navigate their progress across quests. For them to know locked and unlocked nodes as they continue to develop their learning path from these contents. Additionally, it bridges gamification to their overall experience in these quest content

Goals and Non-Goals: Learners will have the ability to check what particular number (similar to book page) to where they at right now. They also have the source to check what achievements (badges) they have gathered while progressing in the quest.


HIGH-LEVEL ARCHITECTURE

System Diagram

Technologies Used:

  • Frontend: Next.js, Tailwind CSS and Shadcn Components

  • Backend: RESTful APIs, Typscript, ZOD Validation, Clerk and Supabase


DETAILED DESIGN AND IMPLEMENTATION

Schema:

The quests can be navigated via these APIs /api/learner/update-progress and /api/learner/get-enrollment which are connected to Save State and Enrollment module of WyzQuests.

Learner Global Stats: this table stores relevant learner details such as number of completed courses, completed chapters, completed lessons, product purchases, community posts and community interactions. Where it is automatic once learner state of progress updates via hardcode in API specification.

This table also is connected to Global Gamification’s trigger.

create table public.learner_global_stats (
user_id uuid not null,
xp_rewards integer null default 0,
completed_courses integer null default 0,
completed_chapters integer null default 0,
completed_lessons integer null default 0,
product_purchases integer null default 0,
community_posts integer null default 0,
community_interactions integer null default 0,
created_at timestamp without time zone null default CURRENT_TIMESTAMP,
updated_at timestamp without time zone null default CURRENT_TIMESTAMP,
current_rank integer null default 0,
previous_rank integer null default 0,
badge_triggers jsonb null default '[]'::jsonb,
constraint learner_global_stats_pkey primary key (user_id),
constraint learner_global_stats_user_id_fkey foreign KEY (user_id) references app_users (id) on delete CASCADE,
constraint learner_global_stats_completed_chapters_check check ((completed_chapters >= 0)),
constraint learner_global_stats_completed_courses_check check ((completed_courses >= 0)),
constraint learner_global_stats_completed_lessons_check check ((completed_lessons >= 0)),
constraint learner_global_stats_current_rank_check check ((current_rank >= 0)),
constraint learner_global_stats_previous_rank_check check ((previous_rank >= 0)),
constraint learner_global_stats_product_purchases_check check ((product_purchases >= 0)),
constraint learner_global_stats_community_interactions_check check ((community_interactions >= 0)),
constraint learner_global_stats_xp_rewards_check check ((xp_rewards >= 0)),
constraint learner_global_stats_community_posts_check check ((community_posts >= 0))
) TABLESPACE pg_default;
 
create index IF not exists idx_learner_global_current_rank on public.learner_global_stats using btree (current_rank desc) TABLESPACE pg_default;
 
create index IF not exists idx_learner_global_stats_xp on public.learner_global_stats using btree (xp_rewards desc) TABLESPACE pg_default;
 
create index IF not exists idx_learner_global_stats_courses on public.learner_global_stats using btree (completed_courses desc) TABLESPACE pg_default;
 
create trigger trigger_update_learner_global_stats_updated_at BEFORE
update on learner_global_stats for EACH row
execute FUNCTION update_global_gamification_updated_at ();

Learner Achievements: this table stores the acquired achievements (badges) which corresponds to specific trigger that is connected to a learner stats. It has the information of locked and unlocked which will be reflected in learner view.

create table public.learner_achievements (
user_id uuid not null,
achievement_id uuid not null,
progress integer null default 0,
unlocked_at timestamp without time zone null,
is_quest boolean null default false,
created_at timestamp without time zone null default CURRENT_TIMESTAMP,
updated_at timestamp without time zone null default CURRENT_TIMESTAMP,
constraint learner_achievements_pkey primary key (user_id, achievement_id),
constraint learner_achievements_achievement_id_fkey foreign KEY (achievement_id) references global_achievements (id),
constraint learner_achievements_user_id_fkey foreign KEY (user_id) references app_users (id) on delete CASCADE,
constraint learner_achievements_progress_check check (
(
(progress >= 0)
and (progress <= 100)
)
)
) TABLESPACE pg_default;
 
create index IF not exists idx_learner_achievements_user on public.learner_achievements using btree (user_id) TABLESPACE pg_default;
 
create index IF not exists idx_learner_achievements_achievement on public.learner_achievements using btree (achievement_id) TABLESPACE pg_default;
 
create index IF not exists idx_learner_achievements_unlocked on public.learner_achievements using btree (unlocked_at desc) TABLESPACE pg_default
where
(unlocked_at is not null);
 
create index IF not exists idx_learner_achievements_quest on public.learner_achievements using btree (is_quest) TABLESPACE pg_default;
 
create trigger trigger_update_learner_achievements_updated_at BEFORE
update on learner_achievements for EACH row
execute FUNCTION update_global_gamification_updated_at ();

API Specification:

POST /api/learner/achievements/progress
Updates learner's achievement progress

Body: { achievement_id: string, progress: number, is_quest?: boolean }

Status

Return

401

Unauthorized

400 (Null body)

Missing required fields

400 (Incorrect input)

Progress must be between 0 and 100

404 (Validate User)

User not found

500

error.message

200 OK

Data: user_id, achievement_id

GET /api/learner/achievements
Retrieves all achievements for the authenticated user

Query params: ?is_quest=true/false to filter by type

Status

Return

401

Unauthorized

500 (Fetching Achievements)

Failed to fetch achievements with error.message

200 OK

Data: all details of learner_achievements and global_achievements

POST /api/learner/achievements
Creates or updates an achievement progress for the authenticated user

Body: { achievement_id: string, progress: number, is_quest?: boolean, unlocked_at?: string | null }

Status

Return

401

Unauthorized

400 (Missing fields)

Missing required fields: achievement_id, progress, is_quest

400 (Incorrect range)

Progress must be between 0 and 100

500 (Update

Failed to update achievement

500 (Create)

Failed to create achievement

200 OK (Update)

Achievement updated successfully

200 OK (Create)

Achievement created successfully

GET /api/learner/global-stats
Fetches the current user's global statistics

Status

Return

401

Unauthorized

404 (User Validation)

User not found

200 OK (Stats Does not Exist)

Learner stats into 0

500

statsError.message

200 OK

Data: user_id, xp_rewards, completed_courses, completed_chapters, completed_lessons, product_purchases, community_posts, community_interactions

PATCH /api/learner/global-stats
Updates specific learner's global statistics

Body: { xp_rewards?, completed_courses?, completed_chapters?, completed_lessons?, product_purchases?, community_posts?, community_interactions?, badge_triggers? }

Status

Return

401

Unauthorized

404 (User Validation)

User not found

404 (No stat record)

Learner stats not found

500

error.message

200 OK

All columns of learner_global_stats

POST /api/learner/global-stats
Updates specific learner's global statistics

Body: { xp_rewards?: number, completed_courses?: number, completed_chapters?: number, completed_lessons?: number, product_purchases?: number, community_posts?: number, community_interactions?: number, stat_name: string, value: number; }

Status

Return

401

Unauthorized

404 (User Validation)

User not found

404 (No stat record)

Learner stats not found

500

error.message

200 OK

All columns of learner_global_stats

GET /api/learner/leaderboard
Fetches all global achievements with learner's progress

Body: { xp_rewards?: number, completed_courses?: number, completed_chapters?: number, completed_lessons?: number, product_purchases?: number, community_posts?: number, community_interactions?: number, stat_name: string, value: number; }

Status

Return

401

Unauthorized

500

error.message

200 OK

All columns of learner_global_stats

Core Logic and Workflow:

  1. Must be a learner in WyzQuests

  2. This learner must be able to interact within the application to either a quest, community or various products.

  3. Via backend with the APIs listed about, it automatically assigned the progress and achievement.

  4. The process of update continue, but whenever the trigger to an achievement is met to 100% then comparison to that badge halts.


INFRASTRUCTURE AND OPERATIONS

Dependencies: No known major dependencies other than ZOD validation to ensure data inputs and returns are correct and accurate

Monitoring and Alerting:

  • Check via database its egress to ensure that gamification does not take much of the processed data.

  • Other than that, alert notification will be sent whenever internal services do not provide accurate returns or have an error.

Deployment Plan:

  • Ensure that tables learner_global_stats and learner_achievements are migrated to the main database.

  • Ensure that all APIs are configured and is directed only to learners. Logged out users (via share or SCORM) must not bypass these APIs.


TESTING AND QUALITY ASSURANCE

Testing Strategy:

  • Navigate in learner view all possible actions, from quest, purchases and community

  • Multiple learners must act in completing quests to test leaderboard for XP comparison

Known Limitations:

  • Not all badges are implemented yet within the system

  • Per node XP is not implemented for further XP configuration


MAINTENANCE AND SUPPORT

  • Ensure that via Admin there are specified triggers and badges configured connected to learner stats to process achievement whenever learners navigate the application.


Document Version

1.0 - Draft, Feature pushed but in need for further development 07/06/2026




Was this article helpful?