Quick answer: Use the AJAX plugin or the JavaScript fetch() API to send scores to a backend server and retrieve the top scores list. Firebase Realtime Database is the fastest option to get running — it requires no server code and has a free tier. For custom backends, any REST API that accepts JSON and returns sorted scores will work. Add basic anti-cheat by validating scores server-side and rate-limiting submissions.

Every competitive game needs a leaderboard. Whether you are building an endless runner, a puzzle game, or an arcade shooter in Construct 3, players want to see how they rank against others. Construct 3 does not have a built-in leaderboard feature, but its AJAX plugin and scripting API make it straightforward to connect to any backend that speaks HTTP and JSON. This guide covers the full pipeline: choosing a backend, submitting scores, fetching and displaying the leaderboard, and protecting it from fake submissions.

Choosing a Backend

You need somewhere to store scores. The three most common options for Construct 3 games are Firebase Realtime Database, a custom REST API, and third-party leaderboard services.

Firebase Realtime Database is the fastest path. Create a Firebase project, enable the Realtime Database, and set the security rules to allow read access to everyone and write access to authenticated users (or anyone, for a prototype). Firebase provides a REST API that works directly with the AJAX plugin — no SDK or plugin required.

A custom REST API gives you full control. A simple Node.js or Python server with a database (SQLite for small games, PostgreSQL for larger ones) lets you implement custom validation, anti-cheat logic, and complex leaderboard features like daily/weekly/all-time views. Host it on a free tier of Railway, Render, or Fly.io.

Third-party services like LootLocker, GameJolt API, or Newgrounds.io provide leaderboard APIs specifically designed for games. They handle storage, anti-cheat, and often provide player authentication. The tradeoff is vendor lock-in and potential costs at scale.

Submitting Scores with AJAX

Once you have a backend, submitting a score is a single AJAX request. Here is the event sheet approach using Firebase Realtime Database as the backend:

// Event sheet: Submit score to Firebase

// When the game ends, send the score
Function "SubmitScore" (playerName, score)
  ——
  AJAX: Set request header "Content-Type" to "application/json"
  AJAX: Post to URL
    "https://your-project.firebaseio.com/scores.json"
    Data: "{"name":"" & playerName & "","score":" & score & ","timestamp":" & UnixTime & "}"
    Tag: "SubmitScore"

// Handle success
AJAX: On completed "SubmitScore"
  ——
  StatusText: Set text to "Score submitted!"

// Handle error
AJAX: On error "SubmitScore"
  ——
  StatusText: Set text to "Failed to submit score. Check connection."

If you prefer the scripting API, the same operation with fetch() gives you better error handling and async/await support:

// Scripting API: Submit score
async function submitScore(playerName, score) {
  const url = "https://your-project.firebaseio.com/scores.json";

  try {
    const response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: playerName,
        score: score,
        timestamp: Date.now()
      })
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();
    console.log("Score submitted, key:", data.name);
    return true;
  } catch (err) {
    console.error("Submit failed:", err.message);
    return false;
  }
}

Fetching and Displaying Scores

Retrieving scores from Firebase uses a GET request with query parameters to sort and limit results. Firebase supports orderBy, limitToLast, and other query options directly in the URL.

// Event sheet: Fetch top 10 scores from Firebase

System: On start of layout
  ——
  AJAX: Request
    "https://your-project.firebaseio.com/scores.json?orderBy=\"score\"&limitToLast=10"
    Tag: "GetScores"

AJAX: On completed "GetScores"
  ——
  // Parse the response JSON
  JSON: Parse AJAX.LastData
  // Firebase returns an object, not an array
  // Loop through entries and populate the leaderboard UI
  Local variable rank = 0
  JSON: For each key
    ——
    System: Add 1 to rank
    // Create a leaderboard row
    System: Create LeaderboardRow at 100, 80 + (rank * 40)
    LeaderboardRow: Set text to
      rank & ". " & JSON.Get("name") & " - " & JSON.Get("score")

For a polished UI, create a container with a fixed number of row sprites (10 rows for a top-10 board). Each row has a Text object for rank, name, and score. When scores load, populate each row and hide any unused rows. Add a highlight effect to the current player’s row by changing the background color or adding a border.

If your game needs more than a simple top-10 list — for example, showing the player’s rank even if they are not in the top 10, or filtering by time period — a custom backend gives you the query flexibility Firebase lacks. A single SQL query can return both the top 10 and the current player’s rank in one request.

Anti-Cheat Basics

Any leaderboard exposed to the internet will attract fake scores. You cannot fully prevent cheating from a client-side HTML5 game, but you can make it difficult enough that casual cheaters give up.

Server-side validation. Define a maximum possible score based on your game mechanics. If the highest theoretically achievable score in a 3-minute game is 50,000, reject any submission above that. Log suspicious submissions for manual review.

Session tokens. When the game starts, request a session token from your server. The server generates a random token and records the start time. When the player submits a score, include the token. The server checks that the session duration is plausible for the submitted score and that the token has not already been used.

// Scripting API: Token-based score submission

// At game start, get a session token
let sessionToken = null;

async function startSession() {
  const res = await fetch("https://your-api.com/session/start", {
    method: "POST"
  });
  const data = await res.json();
  sessionToken = data.token;
}

// At game end, submit score with token
async function submitVerifiedScore(name, score) {
  const res = await fetch("https://your-api.com/scores", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: name,
      score: score,
      token: sessionToken
    })
  });

  // Server validates: token exists, not expired,
  // session duration matches plausible score, token not reused
  const result = await res.json();
  return result.accepted;
}

Rate limiting. Limit each player (identified by IP or anonymous auth) to one score submission per game session. Reject rapid-fire submissions that suggest automated tools.

Score hashing. Compute a hash of the score combined with a secret salt and the session token. Send the hash alongside the score. The server recomputes the hash and rejects mismatches. This is not bulletproof — a determined cheater can read the salt from your game’s JavaScript — but it stops casual tampering with network requests.

Related Issues

If your AJAX requests fail with CORS errors when deployed, see Fix: Construct 3 AJAX Request CORS Error. For issues with data not persisting between layouts, check Fix: Construct 3 Local Storage Data Not Persisting. If your exported game is not running at all, see Fix: Construct 3 Exported Game Not Running. And for performance concerns with large leaderboard lists, see Fix: Construct 3 Performance Low FPS Lag.

Ship with Firebase first. Migrate to a custom backend when you need anti-cheat or complex queries.