Skip to main content

Overview

The @hyperscape/server package runs the authoritative game server:
  • Fastify 5 HTTP API with rate limiting
  • WebSocket real-time game state
  • PostgreSQL database with Drizzle ORM
  • LiveKit voice chat integration
  • CDN manifest loading at startup
  • Integrated frontend serving (production mode)

Package Location

packages/server/
├── src/
│   ├── database/       # Database schema and queries
│   │   ├── repositories/  # Data access layer
│   │   ├── migrations/    # Database migrations
│   │   └── schema.ts      # Drizzle schema definitions
│   ├── infrastructure/ # Docker, CDN configuration
│   ├── scripts/        # Utility scripts
│   ├── shared/         # Shared server utilities
│   ├── startup/        # Server initialization
│   │   └── routes/     # API route handlers
│   ├── systems/        # Server-specific systems
│   │   ├── ServerNetwork/  # Network handlers
│   │   │   └── handlers/   # Event handlers (bank, trade, friends, etc.)
│   │   ├── TradingSystem/  # Player trading system
│   │   ├── DatabaseSystem/ # Database persistence
│   │   └── ActivityLoggerSystem/  # Activity logging
│   ├── 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:
  1. Server configuration and environment loading
  2. Manifest fetching from CDN (production) or local cache (development)
  3. Database connection (PostgreSQL via Drizzle ORM)
  4. Fastify HTTP server with CORS, rate limiting, and static file serving
  5. WebSocket handlers for real-time multiplayer
  6. Game world and ECS systems
  7. Frontend serving (if client build exists in public/)

Manifest Loading

The server fetches game manifests from CDN at startup:
// From src/startup/config.ts
const MANIFEST_FILES = [
  'biomes.json',
  'npcs.json',
  'items/weapons.json',
  'recipes/smithing.json',
  // ... 25+ manifest files
];

// Fetched from PUBLIC_CDN_URL/manifests/
await fetchManifestsFromCDN(cdnUrl, manifestsDir, nodeEnv);
Behavior:
  • Production/CI: Always fetches from CDN, caches locally
  • Development: Skips fetch if local manifests exist
  • Caching: Manifests cached in world/assets/manifests/ with 5-minute TTL
This eliminates the need for Git LFS in production deployments.

Server Systems

TradingSystem

Located in src/systems/TradingSystem/: Modular player-to-player trading system with:
  • acceptance.ts: Accept/decline logic
  • items.ts: Add/remove items from offers
  • request.ts: Trade initiation with proximity checks
  • swap.ts: Screen transitions (Screen 1 ↔ Screen 2)
  • helpers.ts: Shared utilities and validation
  • types.ts: Type definitions
Features:
  • Two-screen confirmation flow (OSRS-accurate)
  • Wealth transfer indicators
  • Proximity validation (2-tile range)
  • Interface blocking (prevents exploits)
  • Audit logging for all trades

ServerNetwork Handlers

Located in src/systems/ServerNetwork/handlers/:
HandlerPurpose
bank/Banking operations (deposit, withdraw, tabs, coins, equipment)
trade/Trading system handlers
friends.tsFriend management and private messaging
combat.tsCombat actions and validation
inventory.tsInventory management
player.tsPlayer state updates
chat.tsChat message handling
dialogue.tsNPC dialogue
quest.tsQuest progression
resources.tsResource gathering
store.tsShop transactions
prayer.tsPrayer system
home-teleport.tsHome teleport functionality

API Routes

Core Endpoints

RouteMethodPurpose
/statusGETHealth check (used by Railway)
/healthGETLegacy health check
/wsWSGame WebSocket connection
/api/agent/*VariousElizaOS agent API endpoints
/api/character/*VariousCharacter management
/api/player/*VariousPlayer data queries
/api/template/*VariousCharacter templates

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),
});
TablePurpose
usersAccount authentication with Privy/Farcaster IDs
charactersFull character data with all skills and XP
inventoryPlayer items (28 slots with quantities)
equipmentWorn items by slot type
itemsItem definitions and stats
worldChunksPersistent world modifications
playerSessionsLogin/logout tracking
playerDeathsDeath locks to prevent duplication
npcKillsKill statistics per player
friendshipsBidirectional friend relationships
friend_requestsPending friend requests
ignore_listsBlocked players per account

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

Core Configuration

# Server
PORT=5555                    # HTTP/WebSocket port
NODE_ENV=development         # Environment: development, production, test
WORLD=world                  # World directory path

# Database
DATABASE_URL=postgresql://user:pass@host/db    # PostgreSQL connection string
USE_LOCAL_POSTGRES=true      # Auto-start PostgreSQL in Docker (dev only)

# Security
JWT_SECRET=your-jwt-secret   # Token signing (generate with: openssl rand -base64 32)
ADMIN_CODE=your-admin-code   # In-game admin access code

Authentication

# Privy (required for persistent accounts)
PUBLIC_PRIVY_APP_ID=your-privy-app-id
PRIVY_APP_SECRET=your-privy-app-secret
PUBLIC_PRIVY_APP_ID must match between client and server.

Assets & CDN

# CDN URL for game assets (models, textures, audio, manifests)
PUBLIC_CDN_URL=http://localhost:8080           # Development
# PUBLIC_CDN_URL=https://assets.hyperscape.club  # Production

# WebSocket and API URLs (exposed to client)
PUBLIC_WS_URL=ws://localhost:5555/ws
PUBLIC_API_URL=http://localhost:5555
Manifest Loading:
  • Server fetches manifests from PUBLIC_CDN_URL/manifests/ at startup
  • Cached locally in world/assets/manifests/
  • In development, skips fetch if local manifests exist
  • In production/CI, always fetches fresh manifests

Optional Services

# ElizaOS AI Integration
ELIZAOS_API_URL=http://localhost:4001

# LiveKit Voice Chat
LIVEKIT_API_KEY=your-livekit-key
LIVEKIT_API_SECRET=your-livekit-secret
LIVEKIT_URL=wss://your-livekit-server

# Advanced
SAVE_INTERVAL=60             # Auto-save interval in seconds
DISABLE_RATE_LIMIT=false     # Disable rate limiting (dev only!)
COMMIT_HASH=abc123           # Git commit hash (auto-set by CI)

CORS Origins

The server automatically allows requests from:
// Production domains
'https://hyperscape.club'
'https://www.hyperscape.club'
'https://hyperscape.pages.dev'
'https://hyperscape-production.up.railway.app'

// Dynamic patterns
/^https?:\/\/localhost:\d+$/                    // Any localhost port
/^https?:\/\/.+\.hyperscape\.pages\.dev$/       // Cloudflare preview deployments
/^https:\/\/.+\.up\.railway\.app$/              // Railway preview deployments
Additional origins from environment variables:
  • CLIENT_URL
  • PUBLIC_APP_URL
  • ELIZAOS_URL / ELIZAOS_API_URL

Build Scripts

Model Bounds Extraction

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:
  1. Scans world/assets/models/**/*.glb files
  2. Parses glTF position accessor min/max values
  3. Calculates bounding boxes and dimensions
  4. Computes tile footprints at scale 1.0
  5. 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

PackagePurpose
fastifyHTTP server (v5)
@fastify/websocketWebSocket support
@fastify/corsCORS handling
@fastify/rate-limitRate limiting
pgPostgreSQL client
@hyperscape/sharedCore engine
@privy-io/server-authAuthentication
livekit-server-sdkVoice chat
msgpackrBinary 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

Railway (Production)

The server deploys to Railway using Nixpacks for automatic builds: Configuration Files:
  • nixpacks.toml - Build configuration (Bun provider, build commands)
  • railway.server.json - Service configuration (health checks, restart policy)
  • Dockerfile.server - Multi-stage Docker build (alternative to Nixpacks)
Deployment Process:
  1. Push to main branch triggers GitHub Actions
  2. Workflow calls Railway GraphQL API to trigger deployment
  3. Railway builds using Nixpacks configuration
  4. Server starts with cd packages/server && bun dist/index.js
  5. Manifests fetched from Cloudflare R2 CDN at startup
Environment Variables (Railway):
NODE_ENV=production
DATABASE_URL=<auto-set-by-railway-postgres>
JWT_SECRET=<generate-with-openssl>
PRIVY_APP_ID=<from-privy-dashboard>
PRIVY_APP_SECRET=<from-privy-dashboard>
PUBLIC_CDN_URL=https://assets.hyperscape.club
CI=true
SKIP_ASSETS=true
Health Check: Railway monitors /status endpoint every 30 seconds. Server must respond within 3 seconds or it will be restarted.

Manifest Fetching at Startup

In production, the server fetches manifests from the CDN instead of bundling them:
// packages/server/src/startup/config.ts
await fetchManifestsFromCDN(CDN_URL, manifestsDir, NODE_ENV);
Fetched Manifests:
  • Root: npcs.json, stores.json, world-areas.json, etc.
  • Items: items/weapons.json, items/tools.json, etc.
  • Gathering: gathering/woodcutting.json, gathering/mining.json, etc.
  • Recipes: recipes/cooking.json, recipes/smithing.json, etc.
Caching:
  • Manifests cached locally in packages/server/world/assets/manifests/
  • Only re-fetched if content changes (HTTP ETag validation)
  • 5-minute cache headers (Cache-Control: public, max-age=300, must-revalidate)
This allows updating game content (items, NPCs, recipes) without redeploying the server—just upload new manifests to R2.

Key Files

FilePurpose
src/index.tsServer entry point
src/database/Database schema and queries
src/startup/Initialization logic
docker-compose.ymlDocker service configuration
.env.exampleEnvironment template