Node server in production
@rpgjs/server/node lets you run the RPGJS server runtime without Vite.
If this is your first deployment, complete the shared setup in
Put an MMORPG online first. This page then takes the
starter through a Docker deployment. The container serves the browser client,
HTTP room routes, and WebSockets from the same public origin.
If you want to structure an MMORPG project with a framework-agnostic src/server.ts plus host-specific entries such as Express, read /advanced/mmorpg-entries first.
Use it when you want to mount the server in your own Node stack:
- Express
- Fastify
- Hono on Node
- a custom
http.createServer()setup
Dev vs production
In development with@rpgjs/vite, Vite acts as a trusted map publisher. It
builds the map payload and calls the same administration endpoint used by an
editor or deployment pipeline. Gameplay clients never publish map definitions.
In production, map updates must come from a trusted backend source. To protect /map/update, set RPGJS_MAP_UPDATE_TOKEN.
When this environment variable is set:
- gameplay clients cannot update maps
- trusted backend code must send the token
- you can use
transport.updateMap()or call the HTTP endpoint yourself
1. Install the Node adapter dependencies
From the starter project:express and ws must be production dependencies because the built adapter
imports them when the container starts.
2. Create the transport
Createsrc/entries/express.ts. This version fails at startup if the map update
token is missing, stores room state on a configurable SQLite path, serves the
built client, and exposes a health check for the container host.
3. Build the MMORPG
Configure theentryPoints.mmorpg.adapters.express entry and the
build:mmorpg script shown in Put an MMORPG online, then
run:
dist/client contains images and browser assets but no .tmx or
.tsx source maps.
4. Create the Docker image
CreateDockerfile at the project root:
.dockerignore:
.env.production file:
.env.production is ignored by Git, then run:
http://localhost:3000/healthz, then http://localhost:3000.
5. First production deployment
Push the image to the registry supported by your container host. Configure the host with:- container port
3000 RPGJS_MAP_UPDATE_TOKENas a secret- a persistent volume mounted at
/data - health check path
/healthz - HTTPS with WebSocket upgrades enabled
https://game.example.com.
Create an uncommitted .env.publisher on the trusted development or CI machine:
Published map: simplemap message confirms that the server accepted
and persisted the update. Then open the public URL in two independent browser
sessions.
Push trusted map updates from application code
If your map data is produced inside the same trusted Node process, usetransport.updateMap().
transport.updateMap("town", ...) targets the room map-town automatically.
Call /map/update from another trusted backend
If your editor pipeline, admin API, or deploy step runs outside the game server process, call the endpoint directly with the token.
Endpoint format:
x-rpgjs-map-update-token: <token>Authorization: Bearer <token>
Recommended production flow
- Start your Node server with
RPGJS_MAP_UPDATE_TOKENset. - Mount
transport.handleNodeRequest()andtransport.handleUpgrade(). - Keep
initializeMaps: falsein production. - From a trusted backend source, call
transport.updateMap()orPOST /parties/main/map-<id>/map/update. - Let gameplay clients use only normal movement and game actions.
LocalStorageSaveStorageStrategy only works for standalone browser
games. Configure a server-side save strategy before relying on save slots or
account persistence in an MMORPG.
Memory snapshots and SQLite ownership
The memory provider can snapshot all party and room values for a test, a local tool, or an explicit short-lived backup:snapshot() returns an independent representation of the current data.
clear() and restore() replace the provider’s room registry, so reacquire
room handles afterward. Memory data is process-local and disappears when the
process exits unless the application persists the snapshot itself.
For SQLite, pass exactly one database source:
databasePathlets RPGJS open and own the SQLite connection;databasereuses a compatible connection owned by the application.
WebSocket session ids
The Node transport uses the RPGJS room adapter and follows its session model:conn.idis unique for each active WebSocket connection.conn.sessionIdis the stable private session id sent by the client.
provideMmorpg() sends this stable id through PartySocket, so a browser refresh
or a second tab can restore the same player session without replacing the first
active WebSocket. When you need to address or exclude a single physical socket,
use conn.id; when you need to inspect the restored player session, use
conn.sessionId.
Hono and other runtimes
The same transport can be used outside Express:- use
transport.fetch()when your framework exposes FetchRequest/Response - use
transport.handleNodeRequest()when your framework gives Nodereq/res - use
transport.acceptWebSocket()ortransport.handleUpgrade()for WebSocket upgrades
Security note
Do not expose/map/update without a token in public MMORPG production deployments.
/map/update is a trusted server-side operation. It can redefine map geometry, events, and world metadata.
Development with Vite
With@rpgjs/vite, you do not need this manual flow for local development.
The Vite plugin creates the transport internally and performs the server-side map bootstrap automatically.