Vite Plugin
The Colyseus Vite plugin runs your game server inside Vite’s dev server using Vite’s Environment API. A single vite process serves your frontend and hosts the Colyseus server. Hot Module Replacement (HMR) covers both client components and server-side room definitions, with no process restart between edits.
It also drives the production build: vite build emits your static client and a standalone server bundle.
Requirements
- A Vite project (
vite >= 6.0.0). - The
colyseuspackage: the plugin ships with it under thecolyseus/vitesubpath.
Setup
Add the colyseus() plugin to your Vite config, alongside any frontend plugins (such as @vitejs/plugin-react):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { colyseus } from 'colyseus/vite';
export default defineConfig({
plugins: [
react(),
colyseus({
serverEntry: '/src/server/index.ts',
}),
],
});The serverEntry file must export your server configuration as server (from defineServer()) or a rooms map. A default export of { server } / { rooms } also works.
import { defineServer, defineRoom, createRouter, createEndpoint } from "colyseus";
import { MyRoom } from "./MyRoom.ts";
export const server = defineServer({
rooms: {
my_room: defineRoom(MyRoom),
},
// Type-safe HTTP routes, hot-swapped on reload
routes: createRouter({
hello: createEndpoint("/hello", { method: "GET" }, async () => {
return { message: "Hello world!" };
}),
}),
// Express middleware: set up once, persists across reloads
express: (app) => {
app.get('/express-hello', (_req, res) => {
res.json({ message: 'Hello from Express!' });
});
},
});Start everything with the usual Vite command:
npm run devEditing MyRoom.ts reloads the server in place; editing your frontend reloads the browser, both without restarting the process. The Colyseus server attaches to Vite’s own HTTP server, so your client and server share a single origin and port (no dev proxy required).
Options
| Option | Type | Default | Description |
|---|---|---|---|
serverEntry | string | (required) | Path to the module exporting server (or rooms). |
port | number | 2567 | Port the production server listens on. Dev mode reuses Vite’s port. |
serveClient | boolean | false | In the production build, serve the built client via express.static() with an SPA fallback to index.html. No effect in dev. |
quiet | boolean | false | Suppress the plugin’s reload/room logs. |
httpServer | http.Server | — | Required only when running Vite in middleware mode. |
loadWsTransport | () => Promise<…> | — | Provide a custom WebSocket transport loader. Defaults to @colyseus/ws-transport. |
How server HMR works
The plugin enables Colyseus Development Mode internally. When you save a server file:
- The server module is re-imported, picking up your new code.
- HTTP routes and room definitions are swapped for the new ones.
- Active rooms are cached, disposed, and restored: state and seat reservations are preserved, and connected clients reconnect automatically.
Because this reuses the dev-mode machinery, the same hooks and caveats apply:
- Implement
onCacheRoom()/onRestoreRoom()to preserve data held outside the roomstate. - On the frontend, the
onAddschema callbacks fire again after a reload. Be ready to ignore duplicate calls during development.
Like devMode, this is for local development only. The production build (below) runs a normal Colyseus server with HMR disabled.
Production build
vite build --appThis produces:
dist/client/: your static frontend assets.dist/server/server.mjs: a standalone server that imports your entry and callsserver.listen().
Run it with Node:
node dist/server/server.mjsTo have the server also serve the built client (single deployable), enable serveClient:
colyseus({
serverEntry: '/src/server/index.ts',
port: 2567,
serveClient: true,
})With serveClient, the production server mounts dist/client/ via express.static() and adds an SPA fallback that returns index.html for unmatched GET requests.
Middleware mode
In standalone dev mode the plugin attaches the WebSocket transport to Vite’s own HTTP server. When you run Vite in middleware mode (embedding it inside your own Express/HTTP server), that server owns the socket. You must pass it explicitly:
colyseus({
serverEntry: '/src/server/index.ts',
httpServer: myHttpServer,
})Using Nitro
Frameworks such as TanStack Start deploy through Nitro’s Vite plugin. The Colyseus plugin runs next to it, in development and in production. This setup needs colyseus 0.18.6 or later.
Earlier versions fail to load the Colyseus server in development, with TypeError: Cannot read properties of undefined (reading 'import'). Their production build also omits dist/server/server.mjs.
Colyseus with Nitro may not work yet on every production target that Nitro supports. The examples below were tested with Nitro’s Node.js presets, node-server and node-middleware. If you deploy to another target, feedback is welcome via GitHub Issues.
Plugin order
List colyseus() before nitro(). Nitro’s dev server handles every request that reaches it, including the matchmaker routes. If Nitro comes first, those requests return a 404 from your app, and joining a room fails. Your rooms still show as defined in the terminal, so check the plugin order first.
import { defineConfig } from 'vite';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { nitro } from 'nitro/vite';
import viteReact from '@vitejs/plugin-react';
import { colyseus } from 'colyseus/vite';
export default defineConfig({
plugins: [
colyseus({ serverEntry: '/src/server/index.ts' }),
tanstackStart(),
nitro(),
viteReact(),
],
});In development, your pages, the matchmaker and room connections share Vite’s port.
Deploying as two processes
vite build writes two outputs: the Nitro app in .output/, and the Colyseus server in dist/server/server.mjs. Run each one as its own process:
node .output/server/index.mjs # Nitro, port 3000
node dist/server/server.mjs # Colyseus, port 2567The two processes listen on different ports. Point your frontend’s Client at the Colyseus port, which differs from the port it uses in development.
Deploying as one process
To serve everything from the Colyseus server, build Nitro as a Node middleware instead of a standalone server:
nitro({ preset: 'node-middleware', serveStatic: true }),The node-middleware preset makes .output/server/index.mjs export a middleware function instead of starting a server. The serveStatic option makes that middleware serve your frontend’s built assets too. Without it, pages render but their scripts return 404.
Mount the middleware from the express option of your server entry:
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { defineServer, defineRoom, isDevMode } from "colyseus";
import { MyRoom } from "./MyRoom.ts";
export const server = defineServer({
rooms: {
my_room: defineRoom(MyRoom),
},
express: async (app) => {
// In development, Vite serves the Nitro app itself.
if (isDevMode) { return; }
const nitroEntry = pathToFileURL(resolve(".output/server/index.mjs")).href;
const { middleware } = await import(/* @vite-ignore */ nitroEntry);
app.use(middleware);
},
});Colyseus handles its own HTTP routes and WebSocket connections first. Every other request reaches Nitro.
Start the server from your project root. The Nitro path resolves against the working directory, so the server fails to start from anywhere else:
node dist/server/server.mjs- Leave
serveClientoff. Nitro serves your frontend, sodist/client/is never built. WithserveClienton, every page returns a 404. - Keep the
isDevModecheck. The plugin also callsexpressin development, before.output/exists. Without the check, development logs a misleadingExpress not availablewarning.