惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
F
Fortinet All Blogs
Engineering at Meta
Engineering at Meta
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
Intezer
P
Privacy & Cybersecurity Law Blog
M
MIT News - Artificial intelligence
MyScale Blog
MyScale Blog
P
Privacy International News Feed
MongoDB | Blog
MongoDB | Blog
Project Zero
Project Zero
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tenable Blog
Security Latest
Security Latest
Stack Overflow Blog
Stack Overflow Blog
L
Lohrmann on Cybersecurity
V
Vulnerabilities – Threatpost
Microsoft Azure Blog
Microsoft Azure Blog
NISL@THU
NISL@THU
T
Threat Research - Cisco Blogs
L
LangChain Blog
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
SecWiki News
SecWiki News
博客园 - 三生石上(FineUI控件)
Forbes - Security
Forbes - Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
GRAHAM CLULEY
K
Kaspersky official blog
W
WeLiveSecurity
A
Arctic Wolf
TaoSecurity Blog
TaoSecurity Blog
Recorded Future
Recorded Future
AI
AI
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Last Week in AI
Last Week in AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
GbyAI
GbyAI
S
SegmentFault 最新的问题
N
News and Events Feed by Topic
C
CXSECURITY Database RSS Feed - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
罗磊的独立博客

Deno

Deno 2.8 | Deno Claw Patrol: an open-source security firewall for agents | Deno Fresh 2.3: Zero JS by default, View Transitions, and Temporal support | Deno Deno 2.7: Temporal API, Windows ARM, and npm overrides | Deno Build a dinosaur runner game with Deno, pt. 6 | Deno Build a dinosaur runner game with Deno, pt. 5 | Deno Deno Deploy is Generally Available | Deno Introducing Deno Sandbox | Deno Build a dinosaur runner game with Deno, pt. 3 | Deno Build a dinosaur runner game with Deno, pt. 2 | Deno React / Next.js Denial-of-Service Vulnerability: Deno Deploy users protected | Deno Deno 2.6: dx is the new npx | Deno Build a dinosaur runner game with Deno, pt. 1 | Deno React Server Functions / Next.js Vulnerability: Deno Deploy users protected | Deno My highlights from the new Deno Deploy | Deno Deno's Other Open Source Projects | Deno How Deno protects against npm exploits | Deno Help Us Raise $200k to Free JavaScript from Oracle | Deno Deno 2.5: Permissions in the config file | Deno Fresh 2.0 Graduates to Beta, Adds Vite Support | Deno Deno 2.4: deno bundle is back | Deno JavaScript™ Trademark Update | Deno What's coming to JavaScript | Deno A brief history of JavaScript | Deno Reports of Deno's Demise Have Been Greatly Exaggerated | Deno An Update on Fresh | Deno How Plaid migrated 100 services to a new database platform 5x faster with Deno | Deno Deno 2.3: Improved deno compile, local npm packages, and more | Deno Add JSR packages with pnpm and Yarn | Deno Zero-config Debugging with Deno and OpenTelemetry | Deno Exploring Art with TypeScript, Jupyter, Polars, and Observable Plot | Deno Deno v Oracle Update 3: Fighting the JavaScript Trademark | Deno Build a custom RAG AI agent in TypeScript and Jupyter | Deno How to get deep traces in your Node.js backend with OTel and Deno | Deno toranoana.deno #20 登録受付中(2025年3月14日) | Deno Node just added TypeScript support. What does that mean for Deno? | Deno The Dino 🦕, the Llama 🦙, and the Whale 🐋 | Deno Publish a lint rule, get a prize | Deno Deno 2.2: OpenTelemetry, Lint Plugins, node:sqlite | Deno If you're not using npm specifiers, you're doing it wrong | Deno How Deno's documentation is evolving | Deno Oracle justified its JavaScript trademark with Node.js—now it wants that ignored | Deno Introducing the JSR open governance board | Deno Intro to Wasm in Deno | Deno Announcing OpenAI on JSR | Deno Deno in 2024 | Deno Goodbye WinterCG, welcome WinterTC | Deno Build a SolidJS app with Deno | Deno Run your Next.js SSR app on Deno Deploy | Deno Solve Advent of Code 2024 with Deno and Win Prizes! | Deno Deno v. Oracle: Canceling the JavaScript Trademark | Deno Deno 2.1: Wasm Imports and other enhancements | Deno Build a Typesafe API with tRPC and Deno | Deno Self-contained Executable Programs with Deno Compile | Deno Build a Database App with Drizzle ORM and Deno | Deno Introducing your new JavaScript package manager: Deno | Deno Announcing Growthbook on JSR | Deno Build an Astro site with Deno | Deno How to convert CommonJS to ESM | Deno Announcing Deno 2 | Deno The Final Touches: What’s New In v2.0.0-rc.10 | Deno Announcing Stable V8 Bindings for Rust | Deno Deno 2.0 Release Candidate | Deno Secure, efficient private npm registries with Cloudsmith and Deno | Deno Painting the Plane as We Fly It: Designing JSR | Deno Introducing Web Cache API support on Deno Deploy | Deno Deno 1.46: The Last 1.x Release | Deno Protect your cloud spend with new Deno Deploy spend limits | Deno What we got wrong about HTTP imports | Deno Benchmarking AWS Lambda Cold Starts Across JavaScript Runtimes | Deno Announcing Supabase on JSR | Deno Deno 1.45: Workspace and Monorepo Support | Deno Introducing KV Backup for Deno Subhosting | Deno A Gentle Intro to TypeScript | Deno Announcing Hono on JSR | Deno How We Made the Deno Language Server Ten Times Faster | Deno How the Guardian uses Deno to audit accessibility and performance across their 2.7 million articles | Deno Introducing More Flexible Domain Association for Deno Subhosting | Deno The stabilization process of the Standard Library has begun | Deno Deno 1.44: Private npm registries, improved Node.js compat, and performance boosts | Deno How we built a secure, performant, multi-tenant cloud platform to run untrusted code | Deno The Deno Standard Library is now available on JSR | Deno How to document your JavaScript package | Deno Your Low Code Solution Needs an Escape Hatch | Deno Deno 1.43: Improved Language Server performance | Deno How Slack used Deno to save months of engineering effort in launching their new platform | Deno JSR Is Not Another Package Manager | Deno Announcing the Hookdeck SDK on JSR | Deno Announcing the Neon Serverless Driver on JSR | Deno An intro to TSConfig for JavaScript Developers | Deno How we built JSR | Deno How Netlify used Deno Subhosting to build a successful edge functions product | Deno Introducing Simpler Project Creation in Deno Deploy | Deno Deno 1.42: Better dependency management with JSR | Deno Introducing deployctl, the command line interface for Deno Deploy | Deno Introducing JSR - the JavaScript Registry | Deno How to add Monaco to a Next.js app and securely run untrusted user code | Deno Survey Results and Roadmap | Deno Deno 1.41: smaller deno compile binaries | Deno Webhooks suck, but here are alternatives | Deno
Build a dinosaur runner game with Deno, pt. 4 | Deno
Jo Franchett · 2026-01-26 · via Deno

This series of blog posts will guide you through building a simple browser-based dinosaur runner game using Deno.

  1. Setup a basic project
  2. Game loop, canvas, and controls
  3. Obstacles and collision detection
  4. Databases and global leaderboards
  5. Player profiles, customization, and live tuning
  6. Observability, metrics, and alerting

Database Integration & Global Leaderboards

Now that we have a fully playable endless runner, it is time to persist scores. In this post, we will add a database, expose a leaderboard API, and render those rankings inside the UI. Later we can build on this foundation to add player profiles and make the leaderboard global across multiple sessions and devices.

What you’ll learn

By the end of this stage you will have:

  • Set up a PostgreSQL database (locally and in Deno Deploy).
  • Created API endpoints to submit, fetch, and rank player scores.
  • Built a dedicated leaderboard page that auto-refreshes with live data.

1. Build the leaderboard page

We’ll start by creating a new HTML page to display the leaderboard. For now this will only display your top score, but in later stages we will enhance it with global rankings once we introduce player profile settings.

Create a new file called public/leaderboard.html and add the following:

public/leaderboard.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="css/styles.css" />
    <link rel="icon" href="favicon.ico" type="image/x-icon" />
    <title>Dino Runner - Global Leaderboard</title>
  </head>
  <body>
    <main class="leaderboard-container">
      <header>
        <h1>🏆 Global Leaderboard</h1>
        <p>Top players from around the world</p>
      </header>

      <nav>
        <a href="/" class="btn">🎮 Play Game</a>
        <button
          onclick="refreshLeaderboard()"
          class="btn btn-secondary refresh-btn"
        >
          🔄 Refresh
        </button>
      </nav>

      <section class="container" id="leaderboard-content">
        <div class="loading">
          <div>🦕 Loading leaderboard...</div>
        </div>
      </section>

      <div class="last-updated" id="last-updated"></div>
    </main>

    <script src="js/leaderboard.js"></script>
  </body>
</html>

We can also add some basic styling in public/css/styles.css to make the leaderboard look nice:



.leaderboard-list {
  padding: 0;
  border: 1px solid var(--secondary);
}

.leaderboard-entry {
  display: flex;
  padding: 0.5rem 1.5rem;
  justify-content: space-between;
  border-bottom: 1px solid var(--secondary);
}

.leaderboard-entry:last-child {
  border-bottom: none;
}

.leaderboard-rank {
  font-weight: bold;
  color: var(--dark);
  width: 30px;
}

.leaderboard-rank.gold {
  color: #ffd700;
}

.leaderboard-rank.silver {
  color: #c0c0c0;
}

.leaderboard-rank.bronze {
  color: #cd7f32;
}

.leaderboard-name {
  flex-grow: 1;
  margin-left: 15px;
  font-weight: 500;
}

.leaderboard-score {
  font-weight: bold;
  color: var(--primary-dark);
}

.avatar {
  display: inline-block;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: var(--secondary);
  color: var(--dark);
  text-align: center;
  line-height: 40px;
  font-weight: bold;
}

.loading {
  text-align: center;
  padding: 20px;
  color: var(--dark);
  font-style: italic;
}



.leaderboard-grid {
  display: grid;
  grid-template-columns: repeat(5, auto);
  gap: 1rem;
  margin-top: 1rem;
}

.player {
  font-weight: bold;
}

.tl {
  text-align: left;
}

And finally, we’ll add a link to the leaderboard page into the index page. Open public/index.html and add a link under the controls section:

<a href="/leaderboard" class="btn btn-primary btn-block"
>View the Leaderboard</a>

Serve the leaderboard page by short-circuiting Oak before the regular static middleware takes over. In your main.ts file, add the following before the static file serving middleware:

app.use(async (context, next) => {
  if (context.request.url.pathname === "/leaderboard") {
    await context.send({
      root: `${Deno.cwd()}/public`,
      path: "leaderboard.html",
    });
    return;
  }

  await next();
});

2. Wire up the leaderboard script

Create public/js/leaderboard.js to fetch and render leaderboard rows every 30 seconds. For now we’ll only see one row, but later we will expand this to show global rankings.

public/js/leaderboard.js

let leaderboardData = [];

async function loadLeaderboard() {
  try {
    const response = await fetch("/api/leaderboard?limit=50");
    const data = await response.json();

    if (data.success && data.leaderboard) {
      leaderboardData = data.leaderboard;
      renderLeaderboard();
      updateLastUpdated();
      return;
    }

    throw new Error(data.error || "Failed to load leaderboard");
  } catch (error) {
    console.error("Error loading leaderboard", error);
    renderError();
  }
}

function renderLeaderboard() {
  const content = document.getElementById("leaderboard-content");

  if (leaderboardData.length === 0) {
    content.innerHTML = `
      <div class="empty-state">
        <h3>No scores yet!</h3>
        <p>Be the first to set a high score!</p>
        <a href="/" class="btn">Start Playing</a>
      </div>`;
    return;
  }

  const tableHTML = `
    <div class="leaderboard-grid">
      <h4 class="tl">Rank</h4>
      <h4 class="tl">Player</h4>
      <h4>Score</h4>
      <h4>Obstacles</h4>
      <h4>Date</h4>
      ${
    leaderboardData
      .map(
        (entry) => `
          <span class="tl rank-${
          entry.rank <= 3 ? entry.rank : ""
        }">#${entry.rank}</span>
          <div class="player tl">
            <span class="avatar" style="background-color: ${
          playerInitialsToColor(entry.playerName)
        };">
              ${getPlayerInitials(entry.playerName)}
            </span>
            <span>${escapeHtml(entry.playerName)}</span>
          </div>
          <span>${entry.score.toLocaleString()}</span>
          <span>${entry.obstaclesAvoided || 0}</span>
          <span>${formatDate(entry.date)}</span>
        `,
      )
      .join("")
  }
    </div>`;

  content.innerHTML = tableHTML;
}

function renderError() {
  const content = document.getElementById("leaderboard-content");
  content.innerHTML = `
    <div class="error">
      <h3>Unable to load leaderboard</h3>
      <p>Please check your connection and try again.</p>
      <button onclick="refreshLeaderboard()" class="btn refresh-btn">Try Again</button>
    </div>`;
}

function getPlayerInitials(name) {
  return name.slice(0, 2).toUpperCase();
}

function playerInitialsToColor(name) {
  const firstChar = name.charCodeAt(0);
  const hue = (firstChar * 7) % 360;
  return `hsl(${hue}, 70%, 50%)`;
}

function escapeHtml(text) {
  const div = document.createElement("div");
  div.textContent = text;
  return div.innerHTML;
}

function formatDate(dateString) {
  const date = new Date(dateString);
  return date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
}

function updateLastUpdated() {
  const now = new Date();
  document.getElementById("last-updated").textContent =
    `Last updated: ${now.toLocaleTimeString()}`;
}

async function refreshLeaderboard() {
  const content = document.getElementById("leaderboard-content");
  content.innerHTML = `
    <div class="loading">
      <div>Refreshing leaderboard...</div>
    </div>`;
  await loadLeaderboard();
}

setInterval(loadLeaderboard, 30000);
document.addEventListener("DOMContentLoaded", loadLeaderboard);

The helper wraps everything needed to keep the leaderboard fresh client-side: refreshLeaderboard() swaps in a loading state, loadLeaderboard() fetches the latest scores, and the rendering helpers sanitize incoming values, format dates, and hydrate the DOM so you never risk injection issues. A setInterval and a DOMContentLoaded hook ensure the scoreboard initializes when the page loads and keeps polling every 30 seconds without user interaction.

3. Provision a managed PostgreSQL

Head to the Deno Deploy dashboard and create a new managed PostgreSQL database:

  1. Navigate to Databases → + Provision Database.
  2. Choose Provision New Database → Prisma Postgres.
  3. Provide a slug such as dino-runner-db, pick a region close to your players, and click Provision Database.
  4. After the database is ready, click Assign and attach it to your Dino Runner project so Deploy automatically injects credentials.

4. Configure local development

With the [deno –tunnel`](https://docs.deno.com/runtime/reference/cli/run/#options-tunnel) command, you can connect to your managed database from your local machine without needing to set up environment variables or other configuration.

Simply use the run command with the --tunnel flag and Deno will do the rest.

Using a Database url

If you prefer, you can also connect to your managed database using a standard DATABASE_URL connection string (for example if you’re using a local Postgres instance for development). Set up a DATABASE_URL environment variable in your .env file. To get the connection string for your managed database, go to the Deno Deploy dashboard, navigate to your database, and copy the URL from the Databases tab:

Easily copy the database connection string to your clipboard from the Deno Deploy dashboard.

Then, paste it into your .env file. Be sure to append ?sslmode=require to the end of your connection string, which ensures the connection is encrypted via SSL/TLS:

.env

DATABASE_URL=postgresql://your_db_url@db.prisma.io:port?sslmode=require

5. Create the database connection helper

Place the following file at src/database/connection.ts. It lazily creates a connection pool and gracefully degrades through three credential sources.

src/database/connection.ts

import { Pool } from "npm:pg";

let pool: Pool | null = null;

export function getDatabase(): Pool {
  if (pool) {
    return pool;
  }

  const databaseUrl = Deno.env.get("DATABASE_URL");

  if (databaseUrl) {
    console.log("🔧 Using DATABASE_URL for connection pool");
    pool = new Pool({ connectionString: databaseUrl, max: 10 });
    return pool;
  }

  const pgHost = Deno.env.get("PGHOST");
  const pgUser = Deno.env.get("PGUSER");

  if (pgHost && pgUser) {
    console.log("🔧 Using Deno Deploy PostgreSQL environment variables");
    pool = new Pool({
      host: pgHost,
      user: pgUser,
      password: Deno.env.get("PGPASSWORD") || undefined,
      database: Deno.env.get("PGDATABASE") || "postgres",
      port: parseInt(Deno.env.get("PGPORT") || "5432"),
      max: 10,
    });
    return pool;
  }

  console.log("🔧 Using custom DB environment variables (local development)");
  pool = new Pool({
    host: Deno.env.get("DB_HOST") || "localhost",
    user: Deno.env.get("DB_USER") || "postgres",
    password: Deno.env.get("DB_PASSWORD") || undefined,
    database: Deno.env.get("DB_NAME") || "dino_runner",
    port: parseInt(Deno.env.get("DB_PORT") || "5432"),
    max: 10,
  });

  console.log("Database pool created successfully");
  return pool;
}

export async function closeDatabase(): Promise<void> {
  if (!pool) return;
  await pool.end();
  pool = null;
  console.log("Database pool closed");
}

At the bottom of the same file, lets release clients back to the pool:

const db = getDatabase();
const client = await db.connect();
try {
  const result = await client.query("SELECT 1");
  console.log(result.rows);
} finally {
  client.release();
}

6. Define the schema

Create src/database/schema.sql (or a migration) to store scores and the metadata we collect from the game client.

CREATE TABLE IF NOT EXISTS scores (
  id BIGSERIAL PRIMARY KEY,
  player_name TEXT NOT NULL,
  score INTEGER NOT NULL CHECK (score >= 0),
  obstacles_avoided INTEGER DEFAULT 0,
  game_duration_ms INTEGER DEFAULT 0,
  max_speed NUMERIC(6, 2) DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS scores_score_idx
  ON scores (score DESC, created_at DESC);

7. Migrate the database

Make a simple migration script to run the schema against the database. This script, which will create the necessary tables and columns for the global leaderboard, must be executed before the game starts. Create src/database/migration.ts:

src/database/migration.ts

import { getDatabase } from "./connection.ts";

export async function initializeDatabase(): Promise<void> {
  const pool = getDatabase();

  console.log("🚀 Initializing database schema...");

  const schema = await Deno.readTextFile("./src/database/schema.sql");

  
  const client = await pool.connect();
  try {
    await client.query(schema);
    console.log("✅ Database schema initialized successfully");
  } finally {
    client.release(); 
  }
}

export function runMigrations(): void {
  
  console.log("📦 No pending migrations");
}

Then, call it from your main.ts before starting the server:

import { initializeDatabase } from "./database/migration.ts";
await initializeDatabase();

8. Implement leaderboard API routes

We will have to add a router to handle API routes for /api/leaderboard and /api/scores for returning the right data for the leaderboard and scores. Let’s create the file /src/routes/leaderboard.routes.ts and fill it with the below:

/src/routes/leaderboard.routes.ts

import { Router } from "jsr:@oak/oak/router";
import { getDatabase } from "../database/connection.ts";

const leaderboardRouter = new Router();

leaderboardRouter
  .get("/api/leaderboard", async (context) => {
    const limit = Number(context.request.url.searchParams.get("limit")) || 10;
    const client = await getDatabase().connect();

    try {
      const result = await client.query(
        "SELECT player_name, score, obstacles_avoided, created_at FROM scores ORDER BY score DESC, created_at ASC LIMIT $1",
        [limit],
      );

      const leaderboard = result.rows.map((row, index) => ({
        rank: index + 1,
        playerName: row.player_name,
        score: Number(row.score),
        obstaclesAvoided: Number(row.obstacles_avoided || 0),
        date: row.created_at,
      }));

      context.response.body = { success: true, leaderboard };
    } finally {
      client.release();
    }
  })
  .post("/api/scores", async (context) => {
    const body = await context.request.body({ type: "json" }).value;
    const { playerName, score, obstaclesAvoided = 0, gameDuration, maxSpeed } =
      body;

    if (!playerName || typeof score !== "number") {
      context.response.status = 400;
      context.response.body = { success: false, error: "Invalid payload" };
      return;
    }

    const client = await getDatabase().connect();
    try {
      const insertResult = await client.query(
        `INSERT INTO scores (player_name, score, obstacles_avoided, game_duration_ms, max_speed)
         VALUES ($1, $2, $3, $4, $5)
         RETURNING id`,
        [playerName, score, obstaclesAvoided, gameDuration ?? 0, maxSpeed ?? 0],
      );

      const rankResult = await client.query(
        `SELECT COUNT(*) + 1 AS rank
         FROM scores
         WHERE score > $1 OR (score = $1 AND created_at < NOW())`,
        [score],
      );

      context.response.body = {
        success: true,
        id: Number(insertResult.rows[0].id),
        globalRank: Number(rankResult.rows[0].rank) || 1,
        isNewRecord: Number(rankResult.rows[0].rank) === 1,
      };
    } finally {
      client.release();
    }
  });

export default leaderboardRouter;

BigInt note: PostgreSQL numeric types can bubble up as JavaScript bigint. Convert them to Number (as shown above) before serializing to JSON to avoid runtime errors.

After you have created leaderboardRouter, we’ll need to import that into main.ts and include those routes in our app:

src/main.ts



import leaderboardRouter from "./routes/leaderboard.routes.ts";



app.use(leaderboardRouter.routes());
app.use(leaderboardRouter.allowedMethods());

9. Push scores from the game client

Extend public/js/game.js with a helper function submitScoreToDatabase that posts the score after each run.

Note that for now, you can hard code your playerName, but in the next lesson, we will show you how you can create player profiles that allow users to update it themselves.

 async submitScoreToDatabase(gameDuration) {
    
    this.playerName = "Jo";

    try {
      const response = await fetch("/api/scores", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          playerName: this.playerName,
          score: Math.floor(this.score),
          obstaclesAvoided: this.obstaclesAvoided,
          gameDuration
        })
      });

      if (!response.ok) throw new Error("Failed to submit score");

      const data = await response.json();
      if (data.isNewRecord) {
        console.log("🏆 NEW GLOBAL RECORD!");
        if (typeof this.showNewRecordMessage === "function") {
          this.showNewRecordMessage();
        }
      }

      console.log(`Score submitted! Global rank: #${data.globalRank}`);
      if (typeof this.loadGlobalLeaderboard === "function") {
        this.loadGlobalLeaderboard();
      }
    } catch (error) {
      console.error("Failed to submit score", error);
    }
  }

Then update the game over logic to call this function:

We will have to make a few other updates to public/js/game.js so that we can pass the right information in a function call in submitScoreToDatabase. First, let’s update startGame to instantiate a new private variable, gameStartTime, which will be used to calculate the duration the player played in a game.

public/js/game.js


startGame() {
  
  this.gameStartTime = Date.now();
}

Next, we’ll have to update our gameOver logic to calculate gameDuration and to call submitScoreToDatabase:

public/js/game.js

gameOver() {
  

  const gameDuration = Math.floor((Date.now() - this.gameStartTime) / 1000);
  this.submitScoreToDatabase(gameDuration);
}

Once you have done that, we can start the server with deno task dev, play a round, and then see our score on the global leaderboard:

Screenshot of the global leaderboard

Project structure recap

Runner Game/
├── src/
│   ├── database/
│   │   ├── connection.ts
│   │   └── schema.sql
│   ├── middleware/
│   ├── routes/
│   │   └── leaderboard.routes.ts
│   └── main.ts
├── public/
│   ├── index.html
│   ├── leaderboard.html
│   └── js/
│       ├── game.js
│       └── leaderboard.js
├── deno.json
└── README.md

Stage 4 accomplishments

✅ Persistent PostgreSQL storage for scores and analytics metrics.

✅ Global leaderboard API (GET /api/leaderboard, POST /api/scores) with rank calculation.

✅ Dedicated leaderboard page plus in-game auto-refresh to keep data in sync.

✅ Robust connection pooling, environment-variable fallbacks, and BigInt-safe serialization.

What’s next?

In Stage 5, we will lean into personalization: player profiles, cosmetic customization. Once we have player profiles, we can make the leaderboard truly global by associating scores with user accounts. We’ll update our database to store user preferences and scores.