Overview
The @hyperscape/server package runs the authoritative game server:
- Fastify 5 HTTP API with rate limiting
- WebSocket real-time game state
- PostgreSQL (production) / SQLite (local) database
- LiveKit voice chat integration
Package Location
packages/server/
├── src/
│ ├── database/ # Database schema and queries
│ ├── infrastructure/ # Docker, CDN configuration
│ ├── scripts/ # Utility scripts
│ ├── shared/ # Shared server utilities
│ ├── startup/ # Server initialization
│ ├── systems/ # Server-specific systems
│ ├── types/ # TypeScript types
│ ├── types.ts # Core type definitions
│ └── index.ts # Entry point
├── scripts/
│ ├── extract-model-bounds.ts # Build-time model bounds extraction
│ └── inject-model-collision.ts # GLB collision data injection
├── world/
│ └── assets/ # Game assets (Git LFS)
│ ├── models/ # 3D models (GLB files)
│ └── manifests/ # Generated manifests
│ ├── model-bounds.json # Auto-generated model bounds
│ └── stations.json # Station configurations
├── docker-compose.yml # Docker services
├── turbo.json # Turbo build configuration
└── .env.example # Environment template
Entry Point
src/index.ts initializes:
- Database connection (PostgreSQL or SQLite)
- Asset CDN (Docker nginx, optional)
- Fastify HTTP server with plugins
- WebSocket handlers
- Game world and systems
API Routes
| Route | Method | Purpose |
|---|
/health | GET | Health check |
/auth | POST | Authentication |
/ws | WS | Game WebSocket connection |
Database
The server supports both PostgreSQL (production via Neon) and SQLite (local development). Schema is defined with Drizzle ORM.
Key Tables
// From packages/server/src/database/schema.ts
export const characters = pgTable("characters", {
id: text("id").primaryKey(),
accountId: text("accountId").notNull(),
name: text("name").notNull(),
// Combat stats
combatLevel: integer("combatLevel").default(3),
attackLevel: integer("attackLevel").default(1),
strengthLevel: integer("strengthLevel").default(1),
defenseLevel: integer("defenseLevel").default(1),
constitutionLevel: integer("constitutionLevel").default(10),
rangedLevel: integer("rangedLevel").default(1),
// Gathering skills
woodcuttingLevel: integer("woodcuttingLevel").default(1),
fishingLevel: integer("fishingLevel").default(1),
firemakingLevel: integer("firemakingLevel").default(1),
cookingLevel: integer("cookingLevel").default(1),
// XP for all skills...
health: integer("health").default(100),
coins: integer("coins").default(0),
positionX: real("positionX").default(0),
positionY: real("positionY").default(10),
positionZ: real("positionZ").default(0),
});
| Table | Purpose |
|---|
users | Account authentication with Privy/Farcaster IDs |
characters | Full character data with all skills and XP |
inventory | Player items (28 slots with quantities) |
equipment | Worn items by slot type |
items | Item definitions and stats |
worldChunks | Persistent world modifications |
playerSessions | Login/logout tracking |
playerDeaths | Death locks to prevent duplication |
npcKills | Kill statistics per player |
Database Commands
cd packages/server
# Using Drizzle Kit
bunx drizzle-kit push # Apply schema to database
bunx drizzle-kit generate # Generate migrations
bunx drizzle-kit studio # Open Drizzle Studio GUI
Environment Variables
# Database (choose one)
DATABASE_URL=postgresql://user:pass@host/db # PostgreSQL (production)
# or SQLite is used automatically for local development
# PostgreSQL Connection Pool (increased March 2026)
POSTGRES_POOL_MAX=20 # Up from 10 (commit 24fa8a5)
POSTGRES_POOL_MIN=2 # Minimum idle connections
# Database Mode (auto-detected from DATABASE_URL hostname)
# DUEL_DATABASE_MODE=remote # or local (auto-detected)
# Authentication (required for persistent accounts)
PRIVY_APP_ID=your-privy-app-id
PRIVY_APP_SECRET=your-privy-app-secret
JWT_SECRET=your-jwt-secret
# Server Configuration
PORT=5555
NODE_ENV=development
# CDN (optional)
PUBLIC_CDN_URL=http://localhost:8080
DUEL_PUBLIC_CDN_URL=https://assets.hyperscape.club # Production CDN (commit 2b3cbcb)
# Voice Chat (optional)
LIVEKIT_API_KEY=your-livekit-key
LIVEKIT_API_SECRET=your-livekit-secret
LIVEKIT_URL=wss://your-livekit-url
# ElizaCloud AI (for duel arena agents - March 2026)
ELIZAOS_CLOUD_API_KEY=your-elizacloud-api-key # Access to 13 frontier models
# Streaming (optional - auto-detected from available keys)
TWITCH_STREAM_KEY=live_123456789_abcdefghij # or TWITCH_RTMP_STREAM_KEY
KICK_STREAM_KEY=your-kick-stream-key
STREAM_ENABLED_DESTINATIONS=twitch,kick # Auto-detected if not set
# Streaming Capture (March 2026)
STREAM_CAPTURE_MODE=mediarecorder # Changed from cdp (commit 72c667a)
STREAM_CAPTURE_CHANNEL=chrome-beta # Changed from chrome-unstable (commit 547714e)
STREAM_CAPTURE_ANGLE=default # Changed from vulkan (commit 547714e)
DISPLAY=:99 # Xvfb virtual display
# Duel Arena Oracle (optional)
DUEL_ARENA_ORACLE_ENABLED=true
DUEL_ARENA_ORACLE_PROFILE=testnet # or mainnet
DUEL_ARENA_ORACLE_EVM_PRIVATE_KEY=0x...
DUEL_ARENA_ORACLE_SOLANA_AUTHORITY_SECRET=...
Build Scripts
The server includes build-time scripts for automatic collision footprint detection:
# Extract bounding boxes from GLB models
bun run extract-bounds
# Generates: world/assets/manifests/model-bounds.json
How it works:
- Scans
world/assets/models/**/*.glb files
- Parses glTF position accessor min/max values
- Calculates bounding boxes and dimensions
- Computes tile footprints at scale 1.0
- Writes manifest for runtime use
Turbo Integration:
- Runs automatically before
build and dev commands
- Cached based on GLB file changes
- Only rebuilds when models are added/modified
Example Output:
{
"generatedAt": "2026-01-15T11:25:00.000Z",
"tileSize": 1.0,
"models": [
{
"id": "furnace",
"assetPath": "asset://models/furnace/furnace.glb",
"bounds": {
"min": { "x": -0.755, "y": 0.0, "z": -0.725 },
"max": { "x": 0.755, "y": 2.1, "z": 0.725 }
},
"dimensions": { "x": 1.51, "y": 2.1, "z": 1.45 },
"footprint": { "width": 2, "depth": 1 }
}
]
}
Footprints are calculated from model dimensions × modelScale from stations.json. A furnace with raw dimensions 1.51×1.45 and scale 1.5 becomes 2.27×2.18 meters → 2×2 tiles.
Running
Development
bun run dev:server # With hot reload and auto-restart
Production
bun run build:server # Build to dist/ (runs extract-bounds automatically)
bun start # Start production server
Server runs at http://localhost:5555 by default.
Dependencies
| Package | Purpose |
|---|
fastify | HTTP server (v5) |
@fastify/websocket | WebSocket support |
@fastify/cors | CORS handling |
@fastify/rate-limit | Rate limiting |
pg | PostgreSQL client |
@hyperscape/shared | Core engine |
@privy-io/server-auth | Authentication |
livekit-server-sdk | Voice chat |
msgpackr | Binary serialization |
Docker Services
The server can use Docker for CDN and PostgreSQL:
# Asset CDN
bun run cdn:up # Start nginx CDN container
bun run cdn:down # Stop CDN
bun run cdn:logs # View CDN logs
bun run cdn:verify # Verify CDN is working
# Asset Management
bun run assets:sync # Sync assets from Git LFS
bun run assets:deploy # Deploy to R2 (production)
Deployment
Cloudflare Workers
bun run deploy # Deploy to staging
bun run deploy:prod # Deploy to production
Railway
The railway.server.json in the project root configures Railway deployment.
ElizaCloud AI Integration (March 2026)
All duel arena AI agents now use @elizaos/plugin-elizacloud for unified model access (commit 4d1eb53).
13 Frontier Models:
American Models:
openai/gpt-5 - GPT-5
anthropic/claude-sonnet-4.6 - Claude Sonnet 4.6
anthropic/claude-opus-4.6 - Claude Opus 4.6
google/gemini-3.1-pro-preview - Gemini 3.1 Pro
xai/grok-4 - Grok 4
meta/llama-4-maverick - Llama 4 Maverick
mistral/magistral-medium - Magistral Medium
Chinese Models:
deepseek/deepseek-v3.2 - DeepSeek V3.2
alibaba/qwen3-max - Qwen 3 Max
minimax/minimax-m2.5 - Minimax M2.5
zai/glm-5 - GLM-5
moonshotai/kimi-k2.5 - Kimi K2.5
bytedance/seed-1.8 - Seed 1.8
Configuration:
# Single API key for all models
ELIZAOS_CLOUD_API_KEY=your-elizacloud-api-key
Benefits:
- Simplified configuration (one API key instead of multiple provider keys)
- Access to 13 frontier models from 13 different providers
- Consistent model routing and error handling
- Reduced dependency complexity
Files:
src/eliza/agentHelpers.ts - Added elizacloud provider
src/eliza/ModelAgentSpawner.ts - Updated model agent spawning
packages/plugin-hyperscape/src/index.ts - Added ElizaCloud plugin types
Duel Arena Oracle (March 2026)
The server publishes duel outcomes to EVM and Solana oracle contracts for verifiable results (commit aecab58).
New Oracle Fields:
damageA - Total damage dealt by participant A
damageB - Total damage dealt by participant B
winReason - Detailed win reason (knockout, timeout, forfeit, draw)
seed - Cryptographic seed for replay verification
replayHashHex - Hash of replay data for integrity verification
resultHashHex - Combined hash of all duel outcome data
Configuration:
# Enable oracle publishing
DUEL_ARENA_ORACLE_ENABLED=true
DUEL_ARENA_ORACLE_PROFILE=testnet # or mainnet
# Metadata API base URL
DUEL_ARENA_ORACLE_METADATA_BASE_URL=https://api.hyperscape.gg/api/duel-arena/oracle
# EVM signer (works across Base, BSC, Avalanche)
DUEL_ARENA_ORACLE_EVM_PRIVATE_KEY=0x...
# Solana signers
DUEL_ARENA_ORACLE_SOLANA_AUTHORITY_SECRET=...
DUEL_ARENA_ORACLE_SOLANA_REPORTER_SECRET=...
Database Schema:
Oracle records are stored in the arena_rounds table with comprehensive outcome data for betting market settlement and replay verification.
Files:
src/oracle/DuelArenaOraclePublisher.ts - Oracle publishing logic
src/oracle/config.ts - Oracle configuration and target management
src/oracle/types.ts - Oracle type definitions
Duel System
The server includes a comprehensive duel arena system with AI combat, streaming, and betting integration.
Combat Roles (PR #933, commit 82ff784)
Duel agents are assigned weighted random combat roles:
- Melee (50%): Bronze weapons (longsword, scimitar, 2h sword)
- Ranged (25%): Shortbow + bronze arrows (500 qty)
- Mage (25%): Staff of air + wind strike autocast + runes
Implementation:
DuelOrchestrator.pickCombatRole(): Weighted random selection
DuelOrchestrator.ensureAgentCombatSetup(): Role-specific gear equipping
DuelOrchestrator.cleanupAgentCombatSetup(): Full gear removal after duel
DuelCombatAI: Adapts style switching based on combat role
Gear Lifecycle:
- Pre-duel: Role selected → gear equipped → food filled → health restored
- Post-duel: Gear removed → food removed → health restored → teleport back
Critical Bug Fixes (PR #933)
Combat State Key Mismatch:
- Issue: CombatStateService syncs abbreviated keys (
data.c/data.ct) but getGameState() only read full keys
- Impact: DuelCombatAI always saw
inCombat=false and flooded executeAttack every tick
- Fix: EmbeddedHyperscapeService now reads both abbreviated and full keys
- File:
packages/server/src/eliza/EmbeddedHyperscapeService.ts
Magic Attack TOCTOU Race:
- Issue: Cooldown checked early but claimed after async
consumeRunesForSpell call
- Impact: Duplicate magic projectiles, double rune consumption
- Fix: Moved cooldown claim and
enterCombat before async rune consumption
- File:
packages/shared/src/systems/shared/combat/CombatSystem.ts
Terrain Flat Zones (commit 7a60135)
DuelArenaVisualsSystem registers flat zones programmatically for all 8 floor areas (6 arenas + lobby + hospital):
- Prevents players/agents from sinking ~0.4m into arena floors
- Terrain height queries return correct floor-level values
- Terrain mesh carved under floors to prevent grass/vegetation clipping
- File:
packages/shared/src/systems/client/DuelArenaVisualsSystem.ts
Key Files
| File | Purpose |
|---|
src/index.ts | Server entry point |
src/database/ | Database schema and queries |
src/startup/ | Initialization logic |
src/arena/ | Duel arena system (betting, combat AI, market maker) |
src/eliza/ | ElizaOS AI agent integration |
src/systems/StreamingDuelScheduler/ | Streaming duel orchestration |
docker-compose.yml | Docker service configuration |
.env.example | Environment template |