Skip to main content
The Campaign System is CodeJam’s structured learning pathway. Progress through tiers (HTML, CSS, JavaScript, Python, C++), unlock challenges, and defeat bosses to advance your coding journey.
The campaign replaces traditional linear tutorials with a game-like progression system that adapts to your skill level and learning pace.

Campaign Architecture

The campaign is organized into tiers containing nodes. Each node represents a learning module, challenge, or boss battle.

Briefing Nodes

Educational content and interactive guides teaching core concepts.

Challenge Nodes

Timed coding challenges testing your skills in game modes.

Boss Nodes

Epic debugging scenarios in Linux sandboxes. Requires boss keys.

Schema: Campaign Nodes

// From convex/schema.ts:110-123
campaign_nodes: defineTable({
  slug: v.string(), // e.g. "node-1-html-basics"
  title: v.string(),
  type: v.union(
    v.literal("briefing"), 
    v.literal("challenge"), 
    v.literal("boss")
  ),
  tier: v.string(), // "html", "css", "js", "python", "cpp", "tailwindcss"
  requires: v.optional(v.array(v.string())), // slugs of parent nodes
  position: v.object({ x: v.number(), y: v.number() }), // Visual layout
  data: v.object({
      description: v.optional(v.string()),
      guideId: v.optional(v.string()), // For briefing nodes
      gameId: v.optional(v.string()), // For challenge/boss nodes
      bossScenario: v.optional(v.string()), // For boss nodes
  }),
})

Node Type Details

Purpose: Teach concepts through interactive guidesData Fields:
  • guideId - Links to a guide in the guides table
  • description - Brief overview of the learning topic
Example:
{
  slug: "node-1-html-basics",
  title: "HTML: The Skeleton",
  type: "briefing",
  tier: "html",
  data: { 
    description: "Learn the structure of the web.",
    guideId: "html-basics"
  }
}
Purpose: Test skills through game modesData Fields:
  • gameId - Links to a game definition (e.g., “function-fury”)
  • description - Challenge objective
Example:
{
  slug: "node-2-function-fury",
  title: "Function Fury",
  type: "challenge",
  tier: "js",
  requires: ["node-1-html-basics"],
  data: {
    description: "Debug the faulty function.",
    gameId: "function-fury"
  }
}
Purpose: Epic debugging scenarios in sandboxesData Fields:
  • bossScenario - Scenario type (e.g., “memory-leak”)
  • gameId - Optional game mode for the boss
  • description - Boss challenge description
Example:
{
  slug: "node-4-boss-html",
  title: "BOSS: DOM Destroyer",
  type: "boss",
  tier: "html",
  requires: ["node-3-css-intro"],
  data: {
    description: "Fix the memory leak in the layout engine.",
    bossScenario: "memory-leak" 
  }
}

Schema: User Progress

Each user’s campaign progress is tracked:
// From convex/schema.ts:125-131
user_campaign_progress: defineTable({
  userId: v.id("users"),
  unlockedNodes: v.array(v.string()), // slugs
  completedNodes: v.array(v.string()), // slugs
  currentTier: v.string(),
  bossKeys: v.array(v.string()), // keys earned from challenges
}).index("by_user", ["userId"])

Progress Fields

  • unlockedNodes - Array of node slugs the user can access
  • completedNodes - Array of node slugs the user has finished
  • currentTier - Active learning track (“html”, “python”, etc.)
  • bossKeys - Keys earned from completing challenges, required for boss battles

Getting Campaign Data

Fetch All Nodes

// From convex/campaign.ts:12-18
export const getCampaignTree = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("campaign_nodes").collect();
  },
});

Fetch User Progress

// From convex/campaign.ts:20-43
export const getUserProgress = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) return null;

    const progress = await ctx.db
      .query("user_campaign_progress")
      .withIndex("by_user", (q) => q.eq("userId", userId))
      .unique();

    if (!progress) {
      // Return default starter state
      return {
        unlockedNodes: ["node-1-html-basics"],
        completedNodes: [],
        currentTier: "html",
        bossKeys: [],
      };
    }
    return progress;
  },
});
New users automatically start with "node-1-html-basics" unlocked. This is the entry point for the HTML tier.

Selecting a Learning Track

Users can switch between language tiers:
// From convex/campaign.ts:45-99
export const setTrack = mutation({
  args: { trackId: v.string() }, // "html", "python", "cpp", etc.
  handler: async (ctx, args) => {
      const userId = await getAuthUserId(ctx);
      if (!userId) throw new Error("Unauthorized");
      
      let progress = await ctx.db
          .query("user_campaign_progress")
          .withIndex("by_user", q => q.eq("userId", userId))
          .unique();
      
      const startNodes: Record<string, string> = {
          "html": "node-1-html-basics",
          "python": "node-1-python-basics",
          "cpp": "node-1-cpp-basics",
          "js": "node-5-js-logic",
          "tailwindcss": "node-1-tailwind-basics"
      };
      
      const newStartNode = startNodes[args.trackId];
      if (!newStartNode) throw new Error("Invalid track");

      // Sync preferredLanguage for better UX
      const langMap: Record<string, string> = {
          "html": "HTML",
          "python": "Python",
          "cpp": "C++",
          "js": "JavaScript",
          "tailwindcss": "CSS"
      };
      if (langMap[args.trackId]) {
          await ctx.db.patch(userId, { 
            preferredLanguage: langMap[args.trackId] 
          });
      }

      if (!progress) {
           await ctx.db.insert("user_campaign_progress", {
              userId,
              unlockedNodes: [newStartNode],
              completedNodes: [],
              currentTier: args.trackId,
              bossKeys: []
          });
      } else {
          // Add to unlocked nodes
          const newUnlocked = [...progress.unlockedNodes];
          if (!newUnlocked.includes(newStartNode)) {
              newUnlocked.push(newStartNode);
          }
          await ctx.db.patch(progress._id, {
              unlockedNodes: newUnlocked,
              currentTier: args.trackId
          });
      }
  }
});

Available Tiers

HTML

Start here. Learn DOM structure and semantic markup.

Python

Data-focused track with logic and algorithms.

JavaScript

Advanced tier unlocked after HTML boss.

C++

Expert-level low-level programming.

Tailwind CSS

Utility-first styling framework.

Completing Nodes

When a user completes a node (finishes a guide or wins a challenge):
// From convex/campaign.ts:101-141
export const internalCompleteNode = internalMutation({
  args: { nodeSlug: v.string(), userId: v.id("users") },
  handler: async (ctx, args) => {
    let progress = await ctx.db
        .query("user_campaign_progress")
        .withIndex("by_user", q => q.eq("userId", args.userId))
        .unique();

    if (!progress) return;

    if (progress.completedNodes.includes(args.nodeSlug)) {
        return; // Already completed
    }

    // Add to completed
    const newCompleted = [...progress.completedNodes, args.nodeSlug];
    
    // Unlock next nodes
    const allNodes = await ctx.db.query("campaign_nodes").collect();
    const newlyUnlocked: string[] = [];
    
    for (const node of allNodes) {
        if (node.requires && node.requires.includes(args.nodeSlug)) {
            const allMet = node.requires.every(req => 
              newCompleted.includes(req)
            );
            if (allMet && !progress.unlockedNodes.includes(node.slug)) {
                newlyUnlocked.push(node.slug);
            }
        }
    }
    
    await ctx.db.patch(progress._id, {
        completedNodes: newCompleted,
        unlockedNodes: [...progress.unlockedNodes, ...newlyUnlocked]
    });
  }
});

Unlocking Logic

1

Node Completed

User finishes a briefing, challenge, or boss node.
2

Add to Completed

The node slug is added to completedNodes array.
3

Check Dependencies

System scans all nodes with requires field containing the completed node.
4

Validate Prerequisites

For each dependent node, verify ALL required nodes are completed.
5

Unlock New Nodes

Nodes with satisfied dependencies are added to unlockedNodes.
This dependency system allows complex node graphs, not just linear progression. You could have nodes requiring multiple prerequisites from different tiers.

Campaign Seed Data

The campaign is initialized with seed data:
// From convex/campaign.ts:153-284 (excerpt)
export const seedCampaign = mutation({
  args: {},
  handler: async (ctx) => {
    const nodes = [
        // HTML Track
        {
            slug: "node-1-html-basics",
            title: "HTML: The Skeleton",
            type: "briefing",
            tier: "html",
            position: { x: 100, y: 300 },
            data: { 
                description: "Learn the structure of the web.",
                guideId: "html-basics"
            }
        },
        {
            slug: "node-2-function-fury",
            title: "Function Fury",
            type: "challenge",
            tier: "js",
            requires: ["node-1-html-basics"],
            position: { x: 300, y: 300 },
            data: {
                description: "Debug the faulty function.",
                gameId: "function-fury"
            }
        },
        {
            slug: "node-4-boss-html",
            title: "BOSS: DOM Destroyer",
            type: "boss",
            tier: "html",
            requires: ["node-3-css-intro"],
            position: { x: 700, y: 300 },
            data: {
                description: "Fix the memory leak.",
                bossScenario: "memory-leak" 
            }
        },
        // Additional tiers...
    ];

    for (const node of nodes) {
        await ctx.db.insert("campaign_nodes", node);
    }
  }
});
The seed mutation deletes existing nodes before inserting new ones. This is for development only - production systems should use upsert logic.

Visual Roadmap

The campaign system includes position data for visual rendering:
position: { x: v.number(), y: v.number() }
This enables a 2D roadmap UI:
[HTML Basics] --> [Function Fury] --> [CSS Intro] --> [BOSS: DOM Destroyer] --> [JS Logic]
      |
      v
[Python Basics] --> [Logic Labyrinth]
      |
      v
[C++ Basics]
Position coordinates can be used to render an interactive node graph using libraries like React Flow or D3.js.

Boss Keys

Boss nodes require boss keys earned from completing challenges:
bossKeys: v.array(v.string())
When a user completes a challenge node, they earn a boss key. Boss nodes check if the user has the required key before allowing entry.

Boss Key System

Earned From: Challenge nodes
Used For: Unlocking boss battles
Tracked In: user_campaign_progress.bossKeys
Example: Completing 3 challenges in the HTML tier earns an “HTML Boss Key” required to enter the DOM Destroyer boss battle.

Integration with Guides

Briefing nodes link to the Learning Paths system:
// From convex/guides.ts:88-103
// When a guide is completed, check if it's part of a campaign node
const guide = await ctx.db.get(args.guideId);
if (guide) {
  const allNodes = await ctx.db.query("campaign_nodes").collect();
  const node = allNodes.find(n => n.data?.guideId === guide.slug);

  if (node) {
    await ctx.runMutation(internal.campaign.internalCompleteNode, {
      userId,
      nodeSlug: node.slug
    });
  }
}
Completing a guide automatically marks the corresponding campaign node as completed, unlocking the next nodes in the chain.

Dashboard Integration

The campaign system powers the main dashboard view:
  • Roadmap Widget - Visual node graph showing locked/unlocked/completed states
  • Current Tier Display - Shows active learning track
  • Progress Stats - Percentage completion per tier
  • Next Challenge - Suggests the next unlocked node

Example Campaign Flow

1

User Starts Campaign

New user automatically has "node-1-html-basics" unlocked.
2

Complete HTML Basics

User finishes the HTML guide. Node marked complete, unlocks "node-2-function-fury".
3

Win Function Fury

User completes the challenge. Earns a boss key, unlocks "node-3-css-intro".
4

Complete CSS Intro

User finishes CSS guide. Unlocks "node-4-boss-html".
5

Defeat DOM Destroyer

User beats the boss battle. Unlocks JavaScript tier: "node-5-js-logic".

Learning Paths

Explore the guide system powering briefing nodes

Boss Battles

Learn about boss node implementation

Game Modes

See all challenge types available in the campaign

Build docs developers (and LLMs) love