Quickstart


What you'll learn

We're going to build a simple leaderboard module from scratch. Users will be able to authenticate, submit leaderboard scores, and retrieve the top scores.

This will teach you how to build a simple leaderboard module from scratch & all related modules concepts including:

  • Create a module
  • Setup your database
  • Write scripts
  • Write a test

Make sure you've installed the Rivet CLI as described here.



Prerequisites



Step 1: Create project

Create a new directory. Open a terminal in this directory.

Run the following command:

rivet init
Command Line


Step 2: Create module

In the same terminal, run:

rivet create module my_leaderboard
Command Line


Step 3: Write database schema

Edit your modules/my_leaderboard/db/schema.prisma file to look like this:

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

model Scores {
    id        String   @id @default(uuid()) @db.Uuid
    createdAt DateTime @default(now()) @db.Timestamp
    userId    String   @db.Uuid
    score     Int

    @@index([score])
}
Prisma


Step 4: Write submit_score script

We're going to create a submit_score script that will:

  • Throttle requests
  • Validate user token
  • Insert score
  • Query score rank

Create script

In the same terminal, run:

rivet create script my_leaderboard submit_score
Command Line

Add dependencies & make public

Update the modules/my_leaderboard/module.json file to look like this:

{
	"dependencies": {
		"users": {},
		"rate_limit": {}
	},
	"scripts": {
		"submit_score": null
	},
	"public": true,
	"errors": {}
}
JSON

Update request & response

Open modules/my_leaderboard/scripts/submit_score.ts and update Request and Response to look like this:

export interface Request {
	userToken: string;
	score: number;
}

export interface Response {
	rank: number;
}
TypeScript

Throttle requests

At the top the run function in modules/my_leaderboard/scripts/submit_score.ts file, add code to throttle requests:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	await ctx.modules.rateLimit.throttlePublic({ requests: 1, period: 15 });
	// ...
}
TypeScript

Validate user token

Then authenticate the user:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	// ...
	const validate = await ctx.modules.users.authenticateUser({ userToken: req.userToken });
	///
}
TypeScript

Insert score

Then insert the score in to the database:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	// ...
	await ctx.db.scores.create({
		data: {
			userId: validate.userId,
			score: req.score,
		},
	});
	// ...
}
TypeScript

Query rank

Finally, query the score's rank:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	// ...
	const rank = await ctx.db.scores.count({
		where: {
			score: { gt: req.score },
		},
	});

	return {
		rank: rank + 1,
	};
}
TypeScript


Step 5: Create get_top_scores script

Create script

In the same terminal, run:

rivet create script my_leaderboard get_top_scores
Command Line

Make public

Open modules/my_leaderboard/module.json and update the get_top_scores script to be public:

{
	"scripts": {
		"get_top_scores": null
	},
	"public": true
}
JSON

Update request & response

Open modules/my_leaderboard/scripts/get_top_scores.ts and update Request and Response to look like this:

export interface Request {
	count: number;
}

export interface Response {
	scores: Score[];
}

export interface Score {
	userId: string;
	createdAt: string;
	score: number;
}
TypeScript

Throttle requests

At the top the run function in modules/my_leaderboard/scripts/get_top_scores.ts file, add code to throttle the requests:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	await ctx.modules.rateLimit.throttlePublic({});
	// ...
}
TypeScript

Query scores

Then query the top scores:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	// ...
	const rows = await ctx.db.scores.findMany({
		take: req.count,
		orderBy: { score: "desc" },
		select: { userId: true, createdAt: true, score: true },
	});
	// ...
}
TypeScript

Convert rows

Finally, convert the database rows in to Score objects:

export async function run(ctx: ScriptContext, req: Request): Promise<Response> {
	// ...
	const scores = [];
	for (const row of rows) {
		scores.push({
			userId: row.userId,
			createdAt: row.createdAt.toISOString(),
			score: row.score,
		});
	}

	return { scores };
}
TypeScript


Step 6: Start development server

Start development server

In the same terminal, run:

rivet dev
Command Line

Migrate database

You will be prompted to apply your schema changes to the database. Name the migration init (this name doesn't matter):

Migrate Database develop (my_leaderboard) Prisma schema loaded from schema.prisma Datasource "db": PostgreSQL database "my_leaderboard", schema "public" at "host.docker.internal:5432" ? Enter a name for the new migration: › init

Success

You've now written a full module from scratch. You can now generate an SDK to use in your game or publish it to a registry.



Step 7 (optional): Test module

Tests are helpful for validating your module works as expected before running in to the issue down the road. Testing is optional, but strongly encouraged.

Create test

rivet create test my_leaderboard e2e
Command Line

Write test

Update modules/my_leaderboard/tests/e2e.ts to look like this:

import { test, TestContext } from "../module.gen.ts";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import { faker } from "https://deno.land/x/[email protected]/mod.ts";

test("e2e", async (ctx: TestContext) => {
	// Create user & token to authenticate with
	const { user } = await ctx.modules.users.createUser({});
	const { token } = await ctx.modules.users.createUserToken({
		userId: user.id,
	});

	// Create some scores
	const scores = [];
	for (let i = 0; i < 10; i++) {
		const score = faker.random.number({ min: 0, max: 100 });
		await ctx.modules.myLeaderboard.submitScore({
			userToken: token.token,
			score: score,
		});
		scores.push(score);
	}

	// Get top scores
	scores.sort((a, b) => b - a);
	const topScores = await ctx.modules.myLeaderboard.getTopScores({ count: 5 });
	assertEquals(topScores.scores.length, 5);
	for (let i = 0; i < 5; i++) {
		assertEquals(topScores.scores[i].score, scores[i]);
	}
});
TypeScript

Run test

In the same terminal, run:

rivet module test
Command Line

You should see this output once complete:

...test logs... ----- output end ----- e2e ... ok (269ms) ok | 1 passed | 0 failed (280ms)