Skip to content

Commit eb10cf2

Browse files
committed
init: bun websocket game server template
Bun-native WS server with shared msgpack protocol, pub/sub rooms, rate limiting, plus Dockerfile, railway.toml, and docker-compose for deploy.
1 parent 86f4083 commit eb10cf2

File tree

13 files changed

+931
-0
lines changed

13 files changed

+931
-0
lines changed

.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Server port (Railway auto-injects this)
2+
PORT=8080
3+
4+
# Comma-separated allowed origins for WebSocket connections (* = allow all)
5+
ALLOWED_ORIGINS=*
6+
7+
# Server tick rate — snapshots broadcast per second (20 = 50ms between ticks)
8+
SNAPSHOT_HZ=20
9+
10+
# WebSocket keepalive ping interval in milliseconds
11+
KEEPALIVE_MS=30000
12+
13+
# Max messages per second per client (sliding window rate limit)
14+
MAX_MESSAGES_PER_SECOND=60
15+
16+
# Max players per room
17+
MAX_PLAYERS_PER_ROOM=50
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Build & Push Docker Image
2+
3+
on:
4+
push:
5+
branches: [main]
6+
tags: ['v*']
7+
pull_request:
8+
branches: [main]
9+
10+
env:
11+
REGISTRY: ghcr.io
12+
IMAGE_NAME: ${{ github.repository }}
13+
14+
jobs:
15+
build-and-push:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Docker Buildx
26+
uses: docker/setup-buildx-action@v3
27+
28+
- name: Log in to GHCR
29+
if: github.event_name != 'pull_request'
30+
uses: docker/login-action@v3
31+
with:
32+
registry: ${{ env.REGISTRY }}
33+
username: ${{ github.actor }}
34+
password: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: Extract metadata
37+
id: meta
38+
uses: docker/metadata-action@v5
39+
with:
40+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
41+
tags: |
42+
type=ref,event=branch
43+
type=sha,prefix=
44+
type=semver,pattern={{version}}
45+
type=semver,pattern={{major}}.{{minor}}
46+
type=raw,value=latest,enable={{is_default_branch}}
47+
48+
- name: Build and push
49+
uses: docker/build-push-action@v5
50+
with:
51+
context: .
52+
platforms: linux/amd64
53+
push: ${{ github.event_name != 'pull_request' }}
54+
tags: ${{ steps.meta.outputs.tags }}
55+
labels: ${{ steps.meta.outputs.labels }}
56+
cache-from: type=gha
57+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# ── Stage 1: Install deps ──────────────────────────────────────────
2+
FROM oven/bun:1-alpine AS builder
3+
WORKDIR /app
4+
5+
COPY package.json bun.lockb* ./
6+
RUN bun install --frozen-lockfile 2>/dev/null || bun install
7+
8+
COPY tsconfig.json ./
9+
COPY src/ ./src/
10+
11+
# ── Stage 2: Production ───────────────────────────────────────────
12+
FROM oven/bun:1-alpine AS runtime
13+
WORKDIR /app
14+
15+
# Non-root user for security
16+
RUN addgroup -g 1001 -S appgroup && \
17+
adduser -S appuser -u 1001 -G appgroup
18+
19+
COPY package.json bun.lockb* ./
20+
RUN bun install --production 2>/dev/null || bun install && \
21+
rm -rf /root/.bun/install/cache
22+
23+
COPY --from=builder /app/src ./src
24+
COPY --from=builder /app/tsconfig.json ./
25+
26+
USER appuser
27+
28+
ENV PORT=8080
29+
30+
EXPOSE 8080
31+
32+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
33+
CMD wget -qO- http://localhost:8080/health || exit 1
34+
35+
CMD ["bun", "src/index.ts"]

README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# bun-ws-gameserver
2+
3+
Production-grade Bun-native WebSocket game server with room-based architecture, binary protocol (msgpack), server-authoritative tick loop, and per-client rate limiting. 5-8x faster than Node.js `ws` — same protocol, same clients.
4+
5+
[![Deploy on Alternate Futures](https://app.alternatefutures.ai/badge/deploy.svg)](https://app.alternatefutures.ai/deploy/bun-ws-gameserver)
6+
7+
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/YOUR_TEMPLATE_ID?referralCode=YOUR_CODE)
8+
9+
## Features
10+
11+
- **Bun-native WebSocket** — Uses `Bun.serve()` built-in WebSocket (5-8x faster than Node.js `ws`)
12+
- **Room-based architecture**`/ws/:roomId` with auto-created rooms and configurable player caps
13+
- **Binary protocol (msgpack)**~40% smaller payloads than JSON
14+
- **Server-authoritative tick loop** — Configurable Hz for snapshot broadcasting
15+
- **Player state sync** — Position, rotation, action, and timestamp per player
16+
- **Bun pub/sub** — Built-in topic-based broadcasting for efficient room messages
17+
- **Zero-allocation per-connection state**`ws.data` pattern for per-client metadata
18+
- **Per-client rate limiting** — Sliding window algorithm
19+
- **KeepAlive** — Bun's built-in ping/pong with configurable idle timeout
20+
- **Origin allowlist** — Configurable CORS protection
21+
- **Health + Metrics endpoints**`/health` and `/metrics` for monitoring and autoscaling
22+
- **Production Dockerfile** — Multi-stage (oven/bun:1-alpine), non-root user, HEALTHCHECK
23+
24+
## Quick Start
25+
26+
```bash
27+
# Install dependencies
28+
bun install
29+
30+
# Development (with hot reload)
31+
bun run dev
32+
33+
# Production
34+
bun src/index.ts
35+
```
36+
37+
## Docker
38+
39+
```bash
40+
# Build and run
41+
docker compose up --build
42+
43+
# Or manually
44+
docker build -t bun-ws-gameserver .
45+
docker run -p 8080:8080 bun-ws-gameserver
46+
```
47+
48+
## Environment Variables
49+
50+
| Variable | Default | Description |
51+
|----------|---------|-------------|
52+
| `PORT` | `8080` | Server listen port |
53+
| `ALLOWED_ORIGINS` | `*` | Comma-separated allowed origins |
54+
| `SNAPSHOT_HZ` | `20` | Tick rate (snapshots/sec) |
55+
| `KEEPALIVE_MS` | `30000` | Idle timeout (ms) |
56+
| `MAX_MESSAGES_PER_SECOND` | `60` | Per-client rate limit |
57+
| `MAX_PLAYERS_PER_ROOM` | `50` | Room capacity |
58+
59+
## Protocol
60+
61+
Both [`node-ws-gameserver`](https://github.com/alternatefutures/node-ws-gameserver) and `bun-ws-gameserver` use the same **msgpack binary protocol**, so clients are backend-agnostic.
62+
63+
### Client → Server
64+
65+
```typescript
66+
{ type: "join", payload: { displayName: string } }
67+
{ type: "state", payload: { position: {x,y,z}, rotation: {x,y,z,w}, action: string } }
68+
{ type: "chat", payload: { message: string } }
69+
```
70+
71+
### Server → Client
72+
73+
```typescript
74+
{ type: "snapshot", payload: { players: Record<id, PlayerState>, timestamp: number } }
75+
{ type: "player_joined", payload: { id: string, displayName: string } }
76+
{ type: "player_left", payload: { id: string } }
77+
{ type: "chat", payload: { id: string, message: string } }
78+
{ type: "error", payload: { code: string, message: string } }
79+
```
80+
81+
### Example Client (browser)
82+
83+
```typescript
84+
import { encode, decode } from '@msgpack/msgpack';
85+
86+
const ws = new WebSocket('ws://localhost:8080/ws/lobby');
87+
ws.binaryType = 'arraybuffer';
88+
89+
ws.onopen = () => {
90+
ws.send(encode({ type: 'join', payload: { displayName: 'Player1' } }));
91+
};
92+
93+
ws.onmessage = (event) => {
94+
const msg = decode(new Uint8Array(event.data));
95+
if (msg.type === 'snapshot') {
96+
// Update game state with msg.payload.players
97+
}
98+
};
99+
100+
// Send player state at 30fps
101+
setInterval(() => {
102+
ws.send(encode({
103+
type: 'state',
104+
payload: {
105+
position: { x: 0, y: 0, z: 0 },
106+
rotation: { x: 0, y: 0, z: 0, w: 1 },
107+
action: 'idle',
108+
},
109+
}));
110+
}, 33);
111+
```
112+
113+
## Why Bun?
114+
115+
| | Node.js (`ws`) | Bun (native) |
116+
|---|---|---|
117+
| WebSocket throughput | ~50k msg/s | ~400k msg/s |
118+
| HTTP + WS server | Separate setup | Single `Bun.serve()` |
119+
| Per-connection state | WeakMap lookup | `ws.data` (zero-alloc) |
120+
| Broadcasting | Manual loop | Built-in pub/sub topics |
121+
| Startup time | ~200ms | ~20ms |
122+
123+
Same protocol, same clients, same API — just faster.
124+
125+
## Endpoints
126+
127+
| Path | Method | Description |
128+
|------|--------|-------------|
129+
| `/ws/:roomId` | WS | WebSocket game connection (default room: "lobby") |
130+
| `/health` | GET | Health check — status, rooms, connections, uptime |
131+
| `/metrics` | GET | Detailed metrics — memory, messages/sec per room |
132+
133+
## Deploy
134+
135+
### Alternate Futures
136+
137+
Click the deploy button at the top, or go to [app.alternatefutures.ai](https://app.alternatefutures.ai) — select this template and deploy to decentralized cloud in one click.
138+
139+
### Railway
140+
141+
1. Fork this repo
142+
2. Connect to Railway
143+
3. Deploy — Railway reads `railway.toml` automatically
144+
145+
### Docker (any host)
146+
147+
```bash
148+
docker build --platform linux/amd64 -t bun-ws-gameserver .
149+
docker run -p 8080:8080 -e PORT=8080 bun-ws-gameserver
150+
```
151+
152+
## License
153+
154+
MIT

docker-compose.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: '3.8'
2+
3+
services:
4+
gameserver:
5+
build: .
6+
ports:
7+
- "${PORT:-8080}:8080"
8+
environment:
9+
- PORT=8080
10+
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*}
11+
- SNAPSHOT_HZ=${SNAPSHOT_HZ:-20}
12+
- KEEPALIVE_MS=${KEEPALIVE_MS:-30000}
13+
- MAX_MESSAGES_PER_SECOND=${MAX_MESSAGES_PER_SECOND:-60}
14+
- MAX_PLAYERS_PER_ROOM=${MAX_PLAYERS_PER_ROOM:-50}
15+
restart: unless-stopped
16+
healthcheck:
17+
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
18+
interval: 30s
19+
timeout: 5s
20+
retries: 3
21+
start_period: 10s

package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "bun-ws-gameserver",
3+
"version": "1.0.0",
4+
"description": "Production-grade Bun-native WebSocket game server with room-based architecture, binary protocol (msgpack), server-authoritative tick loop, and per-client rate limiting.",
5+
"type": "module",
6+
"main": "src/index.ts",
7+
"scripts": {
8+
"start": "bun src/index.ts",
9+
"dev": "bun --watch src/index.ts",
10+
"typecheck": "bun x tsc --noEmit"
11+
},
12+
"keywords": [
13+
"websocket",
14+
"gameserver",
15+
"realtime",
16+
"multiplayer",
17+
"msgpack",
18+
"rooms",
19+
"tick-loop",
20+
"bun"
21+
],
22+
"author": "Alternate Futures",
23+
"license": "MIT",
24+
"dependencies": {
25+
"@msgpack/msgpack": "^3.0.0-beta2"
26+
},
27+
"devDependencies": {
28+
"@types/bun": "^1.1.0",
29+
"typescript": "^5.5.0"
30+
}
31+
}

railway.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[build]
2+
builder = "DOCKERFILE"
3+
dockerfilePath = "Dockerfile"
4+
5+
[deploy]
6+
startCommand = "bun src/index.ts"
7+
healthcheckPath = "/health"
8+
healthcheckTimeout = 30
9+
restartPolicyType = "ON_FAILURE"
10+
restartPolicyMaxRetries = 5

0 commit comments

Comments
 (0)