Overview

Rivet is built from the ground up to enable as many game developers as possible to write backend code & modify existing modules.


Components

Modules have access to the following core components:

  • Scripts Scripts are used to write TypeScript & Deno code that runs on the backend. Request & response types are automatically validated.
  • Database Modules can write database schemas, query data from scripts with type safety, and automatically create + migrate databases.
  • User-Provided Config Modules can allow projects to pass configuration options to the module.
  • Errors Modules specify the types of errors that can occur in their scripts.

File structure

Required

  • module.json Module configuration file. Documentation
  • scripts/
  • db/ (optional) Database-related files. Documentation
    • schema.ts Database schema.
    • migrations/ Database migrations auto-generated from schema.ts. This directory should not be manually modified unless you know what you are doing.
  • config.ts (optional) Defines user-provided config schema. Documentation
  • public.ts (optional) Exports shared functions & types across modules. Documentation

Developer 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])
}
modules/my_leaderboard/db/schema.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": {}
}
modules/my_leaderboard/module.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;
}
modules/my_leaderboard/scripts/submit_score.ts

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 });
	// ...
}
models/my_leaderboard/scripts/submit_score.ts

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 });
	///
}
models/my_leaderboard/scripts/submit_score.ts

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,
		},
	});
	// ...
}
models/my_leaderboard/scripts/submit_score.ts

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,
	};
}
models/my_leaderboard/scripts/submit_score.ts

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
}
modules/my_leaderboard/module.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;
}
modules/my_leaderboard/scripts/get_top_scores.ts

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({});
	// ...
}
models/my_leaderboard/scripts/get_top_scores.ts

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 },
	});
	// ...
}
models/my_leaderboard/scripts/get_top_scores.ts

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 };
}
models/my_leaderboard/scripts/get_top_scores.ts

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
Terminal Output

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]);
	}
});
modules/my_leaderboard/tests/e2e.ts

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)
Terminal Output