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.