Services
Each GameDatabase instance exposes a set of services, read-and-write APIs over the built-in tables. All examples below assume the exported db instance from the Quick start.
Cloud saves
Per-user save data with monotonic versioning. The slot is optional and defaults to 0. Pass a different slot for multiple save files per player. Pass an expectedVersion for optimistic locking; a stale write throws VersionConflictError.
import { VersionConflictError } from "@colyseus/database";
// save(userId, data, slot = 0, expectedVersion?)
const { version } = await db.saves.save(userId, { hp: 100, level: 7 });
const save = await db.saves.load(userId); // { data, version } | null
try {
// pass the expected version for optimistic locking
await db.saves.save(userId, newData, 0, save.version);
} catch (e) {
if (e instanceof VersionConflictError) {
// client had a stale copy: reload and merge
}
}
await db.saves.listSlots(userId); // [{ slot, version, updatedAt }]
await db.saves.delete(userId); // delete slot 0 for this userLeaderboards
Keep-best scoring with optional seasons.
await db.leaderboards.ensure("global", "Global Leaderboard");
await db.leaderboards.submit("global", userId, 1750); // only kept if it beats the current best
await db.leaderboards.submit("global", userId, 999, "season-1"); // isolated per season
const top = await db.leaderboards.top("global", 10);
const nearby = await db.leaderboards.aroundMe("global", userId, 5);Live configs
Declare typed configuration keys with Standard Schema validators (Zod, Valibot, etc.) via defineConfigs. Values are validated on write and cached in memory. When a presence is provided, the cache is invalidated across all server instances when a value changes (e.g. from an admin panel).
import { defineServer, RedisPresence } from "colyseus";
import { GameDatabase, defineConfigs } from "@colyseus/database";
import { z } from "zod";
const configs = defineConfigs({
matchmaking: z.object({
minPlayers: z.number().int().default(2),
maxPlayers: z.number().int().default(10),
}).default({}), // inner field defaults fill the object
});
export const db = new GameDatabase({
connectionString: process.env.DATABASE_URL,
configsRegistry: configs,
presence: new RedisPresence(), // cross-instance invalidation (optional)
});const mm = await db.configs.get("matchmaking"); // typed: { minPlayers, maxPlayers }
const unsubscribe = db.configs.subscribe("matchmaking", (value) => {
// re-applied live whenever an admin changes it
});Analytics, moderation & notes
await db.analytics.track("match_completed", userId, { mode: "ranked", durationMs: 312_000 });
await db.moderation.setRole(userId, "mod");
await db.moderation.can(userId, "delete", "guilds"); // boolean
await db.notes.add(userId, "Refunded once on 2026-04-12", authorId);
await db.notes.deleteAllForUser(userId); // GDPR cleanupRoom plugins
For common patterns, @colyseus/database ships Room Plugins that wire the services above into the room lifecycle automatically (no manual onJoin/onLeave glue):
CloudSavesPlugin: load a player’s save on join, persist it on leaveLeaderboardsPlugin: submit scores on disposeAnalyticsPlugin: track roomcreate/join/leave/disposeevents
import { Room, definePlugins } from "colyseus";
import { CloudSavesPlugin } from "@colyseus/database";
import { db } from "../app.config";
export class MyRoom extends Room {
plugins = definePlugins([
new CloudSavesPlugin({
database: db,
payload: (room, client) => room.state.players.get(client.sessionId).toJSON(),
apply: (room, client, data) => room.state.players.get(client.sessionId).assign(data),
}),
]);
}See Room Plugins for how plugins attach and expose methods.