Skip to Content
ArchitectureActivity System

Activity System

All learner activities — quizzes, flashcards, matching, interactive components, checkpoints, and the toughest-questions drill — route through a single renderer: ActivityRenderer.jsx.

The type column in the activities table is the canonical format identifier. The context query param (or pathname, for the drill) tells the renderer how the activity was launched and which API to call.


Activity Types

typeDescriptionStart ScreenRenderer
quizMultiple-choice question pool with pass/fail trackingNoQuizActivity
flashcardFlip-card term/definition reviewNoFlashcards component
matchingDrag-and-match term to definitionNoMatching component
classifierRead a scenario, pick one of N labeled choicesYesinteractiveRegistry[component]
true_falseRead a statement, answer True or FalseYesinteractiveRegistry[component]
interactiveFreeform interactive component (diagram, profile, etc.)YesinteractiveRegistry[component]

Start screen rule: quiz, flashcard, and matching skip the start screen because they have their own built-in ready state. All other kinds show a full-page start screen before launching.


URL Structure

All activities use a single route:

/activity/:activityId?context=<context>[&lessonId=<slug>][&type=<kind>]

The toughest-questions drill has its own dedicated route (no DB activity ID needed):

/drill/toughest

Context Values

contextWhen usedData source
lesson-nativeActivity launched from a lesson page (default)Activity row + lessons.quiz_questions
recommendedRecommended activity from the dashboard/api/study/recommended-activity/:id
checkpointMissed checkpoint retry from dashboard/api/study/missed-from-checkpoint/:id
missed-quizFailed quiz retry (flashcard or matching)/api/study/missed-from-quiz/:lessonId
toughest-drillToughest Questions Drill — set by pathname, not param/api/users/me/toughest-questions/drill

The toughest-drill context is resolved automatically when the pathname is /drill/toughest. No ?context= param is used on that route.


Routing Logic

/drill/toughest → context = 'toughest-drill' (from pathname) → calls /api/users/me/toughest-questions/drill → renders QuizActivity with all returned questions, no lesson-native side effects /activity/:activityId?context=lesson-native&lessonId=X → loads activity row by ID → loads lesson by lessonId for quiz_questions → tracks quiz pass/fail, cooldown, attempts /activity/:activityId?context=recommended → calls /api/study/recommended-activity/:id → renders flashcard or matching based on activity.type /activity/:activityId?context=checkpoint → calls /api/study/missed-from-checkpoint/:id → renders as flashcard (missed questions only) /activity/:activityId?context=missed-quiz&lessonId=X&type=flashcard|matching → calls /api/study/missed-from-quiz/:lessonId → renders flashcard or matching based on ?type= param

Toughest Questions Drill

  • Route: /drill/toughest (dedicated, no activity ID in path)
  • Data source: /api/users/me/toughest-questions/drill — returns the student’s most-missed questions
  • Renderer: QuizActivity with all returned questions (no draw_count sampling)
  • Side effects: None — no cooldown, no pass tracking, no lesson-native progress
  • Entry points: ToughestQuestionsZone (dashboard), ExamReadinessPage “Drill These Now” button
  • DB record: seeded as title = 'Toughest Questions Drill', lesson_slug = NULL, type = 'quiz'

Lesson-Native Quiz Rules

  • The UI always shows draw_count (questions per attempt) — never the full pool size
  • Retake lockout: 10 minutes, enforced server-side and persisted in localStorage
  • quiz_passed, quiz_best_score, quiz_attempts_count are server-authoritative — never override client-side

activities Table Schema

The valid columns for inserting activities via migration:

ColumnTypeNotes
typeTEXTquiz, flashcard, matching, classifier, true_false, interactive
titleTEXTDisplay name
topicsJSONBArray of topic tag strings (default {})
lesson_slugTEXTnull for platform-level activities
enabledBOOLEANDefault true

classifier and true_false activities do not use items or choices columns — those do not exist on this table. Content is seeded via migration into quiz_questions or content_blocks.


Minimum Content Thresholds

TypeMinimum before activity feels complete
quiz5 questions in pool
flashcard8 cards
matching6 pairs
classifier / true_false5 items

Adding a New Activity

  1. Insert a row into activities via migration using only valid columns: type, title, topics, lesson_slug, enabled.
  2. If introducing a new type, update resolveRenderer() in ActivityRenderer.jsx.
  3. If it renders via interactiveRegistry, register the component in src/app/components/lesson/interactive/index.js.
  4. Decide whether it needs a start screen. If yes, add a case to resolveStartScreenProps() in ActivityRenderer.jsx.
  5. Update docs/Activities.md in the platform monorepo and this page.
Last updated on
© 2026 GritCert. Internal platform documentation.