Database

Database

The recommended way to persist game data in Colyseus is @colyseus/database, the official persistence layer built on version 1.0.0-rc of Drizzle ORM. A single typed connection backs player accounts, cloud saves, leaderboards, live configs, analytics, moderation, and matchmaking.

A single GameDatabase instance exposes:

  • db.auth: user store for @colyseus/auth: bans, session revocation, password management
  • db.saves: versioned cloud saves with optimistic locking
  • db.leaderboards: keep-best scores, seasons, top-N and around-me queries
  • db.configs: typed, hot-reloadable live-ops configuration
  • db.analytics / db.moderation / db.notes: event tracking, roles, and player notes
  • db.segments: declarative player cohorts for targeting
  • DatabaseDriver: a matchmaking driver reusing the same connection

Colyseus itself remains database-agnostic: if your project needs a different stack, bring your own database and query it directly from your rooms.

@colyseus/database was introduced in Colyseus 0.18. Feedback is welcome on the public roadmap.

Installation

npm install --save @colyseus/database

For PostgreSQL, also install postgres (postgres-js):

npm install --save postgres
⚠️

A postgres:// connection string always opens postgres-js, never pg (node-postgres). Drizzle’s own PostgreSQL guide installs pg, so check which one you have. To keep pg, open the pool yourself and pass it as db.

For embedded PGlite, install @electric-sql/pglite instead:

npm install --save @electric-sql/pglite

Drizzle version

@colyseus/database is built on the 1.0 release candidate of Drizzle ORM and declares drizzle-orm as a peer dependency. Install it from the rc dist-tag, alongside drizzle-kit if you generate migrations. npm’s latest is still the 0.x line, and its column builders fail inside the table factory with colBuilder.build(...).postBuild is not a function.

npm install --save drizzle-orm@rc
npm install --save-dev drizzle-kit@rc

Keep a single copy of drizzle-orm in node_modules. With two copies, the code still runs. tsc then rejects eq(db.tables.users.id, …) and every other place where a column from one copy meets a query builder from the other. npm ls drizzle-orm shows any duplicate.

Quick start

Construct a GameDatabase and pass it to defineServer via the database option. The server boots the database (running migrations) before it starts listening, and automatically mounts the @colyseus/auth routes when that package is installed.

src/app.config.ts
import { defineServer } from "colyseus";
import { GameDatabase } from "@colyseus/database";
 
export const db = new GameDatabase({
    connectionString: process.env.DATABASE_URL,
});
 
export default defineServer({
    database: db,
    // ...rooms, routes, etc.
});

Export the db instance so your rooms can import it. Inside any room lifecycle method, the services are ready to use:

src/rooms/MyRoom.ts
import { Room } from "colyseus";
import { db } from "../app.config";
 
export class MyRoom extends Room {
    async onJoin(client) {
        // client.auth was populated by the default static onAuth,
        // which already rejects banned/revoked tokens automatically.
        await db.saves.load(client.auth.id);
    }
}

Dialects & connection strings

The dialect is auto-detected from the connection string. When omitted, SQLite is used with the default file colyseus.db.

Connection stringDialectNotes
(omitted) or ./game.dbSQLiteFile-based; great for development
:memory:SQLiteEphemeral; useful for tests
postgres://… / postgresql://…PostgreSQLProduction; requires the postgres package (postgres-js, not pg)
pglite://./data / pglite://:memory:PGliteEmbedded Postgres, no external server
src/app.config.ts
// SQLite (development): defaults to colyseus.db
new GameDatabase();
 
// PostgreSQL (production)
new GameDatabase({ connectionString: process.env.DATABASE_URL });
 
// Embedded PGlite, file-backed
new GameDatabase({ dialect: "pglite", connectionString: "pglite://./data" });

Use an existing Drizzle instance

Pass db to reuse a Drizzle instance you already opened. Use this option to run on pg (node-postgres), or to share one pool with the rest of your app. connectionString is ignored, and the dialect is read from the instance unless you pass dialect.

src/app.config.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { GameDatabase } from "@colyseus/database";
 
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
 
export const db = new GameDatabase({
    db: drizzle({ client: pool }),
});

migrations: "auto" runs through the instance’s own client, so any driver Drizzle supports works: postgres-js, node-postgres, PGlite, or node:sqlite. An instance that exposes no client (an HTTP proxy driver, for example) needs migrations: "skip" or { files }. The connection stays yours: shutdown() closes only connections the package opened itself.

Migrations

The migrations option controls how the schema is applied at boot:

  • "auto" (default): creates missing tables with their primary keys, foreign keys, UNIQUE and CHECK constraints, adds missing columns to existing tables, and creates missing indexes on every boot. Idempotent and convenient for development. It never drops a column, changes a type, or adds a constraint to a table that already exists. See Constraints and indexes.
  • "skip": does nothing; you manage the schema externally (e.g. a drizzle-kit migrate step in CI before the server starts).
  • { files: "./drizzle" }: runs SQL migration files generated by drizzle-kit. Drizzle tracks applied migrations, so reruns are safe. See Migrations with drizzle-kit.
src/app.config.ts
new GameDatabase({
    connectionString: process.env.DATABASE_URL,
    migrations: "skip", // schema applied by CI
});

Matchmaking driver

Reuse the same connection as your matchmaking driver (one pool for everything, no separate Redis required):

src/app.config.ts
import { defineServer } from "colyseus";
import { GameDatabase, DatabaseDriver } from "@colyseus/database";
 
export const db = new GameDatabase({ connectionString: process.env.DATABASE_URL });
 
export default defineServer({
    database: db,
    driver: new DatabaseDriver(), // adopts the GameDatabase connection
});

Without options, the driver binds to the most recently constructed GameDatabase. With more than one instance in the process, pass it explicitly: new DatabaseDriver({ database: db }). Custom room-cache columns and the standalone (no GameDatabase) mode are covered on the Driver page.

Next steps

  • Authentication: the built-in @colyseus/auth integration (auto-mounted routes, ban gating, JWT revocation, admin helpers).
  • Services: cloud saves, leaderboards, live configs, analytics, moderation, notes, and the matching room plugins.
  • Customizing built-in table schemas: extend the package’s drizzle tables with your own columns, constraints, and indexes.
  • Bring your own database: ORMs, query builders, Firebase, and the onAuth/onLeave integration patterns.