Skip to main content

Overview

The Hyperscape client automatically deploys to Cloudflare Pages on push to main via GitHub Actions. This provides:
  • Global CDN - Fast loading worldwide
  • Automatic HTTPS - SSL certificates managed by Cloudflare
  • Preview deployments - Every commit gets a preview URL
  • Custom domains - hyperscape.gg, hyperscape.club

Automated Deployment

Workflow Trigger

The .github/workflows/deploy-pages.yml workflow triggers on:
on:
  push:
    branches: [main]
    paths:
      - 'packages/client/**'
      - 'packages/shared/**'
      - 'package.json'
      - 'bun.lockb'
  workflow_dispatch:
Why Trigger on Shared Changes: The client depends on packages/shared for packet definitions. When packets change on the server, the client must rebuild to stay in sync. This prevents “Missing packet handler” errors. Example Scenario:
  1. Server adds new packet type (e.g., worldTimeSync)
  2. Shared package exports new packet definition
  3. Client rebuild triggered automatically
  4. Client includes new packet handler
  5. No runtime errors

Deployment Steps

1. Checkout Code:
- name: Checkout code
  uses: actions/checkout@v4
  with:
    submodules: recursive
2. Setup Bun:
- name: Setup Bun
  uses: oven-sh/setup-bun@v2
  with:
    bun-version: latest
3. Install Dependencies:
- name: Install dependencies
  run: bun install --frozen-lockfile
--frozen-lockfile prevents npm rate-limiting by using only the committed lockfile.
4. Build Client:
- name: Build client (includes shared + physx dependencies via turbo)
  run: bun run build:client
  env:
    NODE_OPTIONS: '--max-old-space-size=4096'
    PUBLIC_PRIVY_APP_ID: ${{ secrets.PUBLIC_PRIVY_APP_ID }}
    PUBLIC_API_URL: https://hyperscape-production.up.railway.app
    PUBLIC_WS_URL: wss://hyperscape-production.up.railway.app/ws
    PUBLIC_CDN_URL: https://assets.hyperscape.club
    PUBLIC_APP_URL: https://hyperscape.gg
5. Deploy to Pages:
- name: Deploy to Cloudflare Pages
  run: |
    # Extract first line of commit message (avoid multi-line issues)
    COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1 | tr -d '"' | cut -c1-100)
    npx wrangler pages deploy dist \
      --project-name=hyperscape \
      --branch=${{ github.ref_name }} \
      --commit-hash=${{ github.sha }} \
      --commit-message="$COMMIT_MSG" \
      --commit-dirty=true
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Multi-Line Commit Message Fix

Problem

Wrangler fails when commit messages contain newlines:
# ❌ Fails
--commit-message="feat: add feature

This is a detailed description"

Solution

Extract only the first line (commit 3e4bb48):
# ✅ Works
COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1 | tr -d '"' | cut -c1-100)
--commit-message="$COMMIT_MSG"
Processing:
  1. Extract first line with head -1
  2. Remove quotes with tr -d '"'
  3. Truncate to 100 chars with cut -c1-100

Production URLs

Primary Domain

https://hyperscape.gg
DNS Configuration:
  • CNAME: hyperscape.gghyperscape.pages.dev
  • Managed by Cloudflare Pages

Alternative Domain

https://hyperscape.club

Preview Deployments

Every commit gets a preview URL:
https://<commit-sha>.hyperscape.pages.dev
Example:
https://37c3629946f12af0440d7be8cf01188465476b9a.hyperscape.pages.dev

Required GitHub Secrets

Configure in Settings → Secrets → Actions:
SecretPurposeExample
CLOUDFLARE_API_TOKENCloudflare API token with Pages write accessabc123...
PUBLIC_PRIVY_APP_IDPrivy app ID (must match server)cmgk4zu56005kjj0bcaae0rei

Create Cloudflare API Token

  1. Go to Cloudflare Dashboard
  2. Navigate to My ProfileAPI Tokens
  3. Click Create Token
  4. Use Edit Cloudflare Workers template
  5. Add AccountCloudflare PagesEdit permission
  6. Copy token and add to GitHub Secrets

Environment Variables

Build-Time Variables

These are baked into the client build:
PUBLIC_PRIVY_APP_ID=cmgk4zu56005kjj0bcaae0rei
PUBLIC_API_URL=https://hyperscape-production.up.railway.app
PUBLIC_WS_URL=wss://hyperscape-production.up.railway.app/ws
PUBLIC_CDN_URL=https://assets.hyperscape.club
PUBLIC_APP_URL=https://hyperscape.gg
Build-time variables cannot be changed after deployment. To update, trigger a new build.

Cloudflare Pages Dashboard

You can also set environment variables in the Cloudflare Pages dashboard:
  1. Go to PageshyperscapeSettingsEnvironment variables
  2. Add variables for Production environment
  3. Trigger a new deployment

Manual Deployment

Using Wrangler CLI

# Install wrangler
npm install -g wrangler

# Login to Cloudflare
wrangler login

# Build client
cd packages/client
bun run build

# Deploy to Pages
npx wrangler pages deploy dist \
  --project-name=hyperscape \
  --branch=main \
  --commit-hash=$(git rev-parse HEAD) \
  --commit-message="Manual deployment"

Using GitHub Actions

Trigger manual deployment from GitHub UI:
  1. Go to Actions tab
  2. Select “Deploy Client to Cloudflare Pages” workflow
  3. Click “Run workflow”
  4. Select branch (usually main)
  5. Choose environment (production or preview)
  6. Click “Run workflow”

Concurrency Control

The workflow uses concurrency control to prevent multiple deployments:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
Behavior:
  • Only one deployment per branch at a time
  • New deployment cancels in-progress deployment
  • Prevents race conditions and wasted resources

Deployment Summary

After successful deployment, the workflow outputs:
## 🚀 Deployment Complete

**Branch:** main
**Commit:** 37c3629946f12af0440d7be8cf01188465476b9a

**URLs:**
- Production: https://hyperscape.gg
- Preview: https://37c3629946f12af0440d7be8cf01188465476b9a.hyperscape.pages.dev

Troubleshooting

Build Failures

Check build logs:
  1. Go to Actions tab in GitHub
  2. Click on failed workflow run
  3. Expand “Build client” step
  4. Check error messages
Common Issues:
  • Missing environment variables
  • TypeScript errors
  • Dependency resolution failures
  • Out of memory (increase NODE_OPTIONS)

Deployment Failures

Check wrangler logs:
  1. Expand “Deploy to Cloudflare Pages” step
  2. Look for wrangler error messages
Common Issues:
  • Invalid CLOUDFLARE_API_TOKEN
  • Project name mismatch
  • Multi-line commit messages (fixed in commit 3e4bb48)

Preview URL Not Working

Check deployment status:
  1. Go to Cloudflare Dashboard
  2. Navigate to Pageshyperscape
  3. Check Deployments tab
  4. Verify deployment succeeded
Common Issues:
  • Deployment still in progress
  • Build failed (check logs)
  • DNS propagation delay (wait 5-10 minutes)

Assets Not Loading

Check CDN URL:
# Should be set to R2 bucket
PUBLIC_CDN_URL=https://assets.hyperscape.club
Check R2 CORS:
# Configure CORS for cross-origin loading
bash scripts/configure-r2-cors.sh
Verify assets exist:
curl -I https://assets.hyperscape.club/models/player.glb

CORS Errors

Problem: Client on hyperscape.gg cannot load assets from assets.hyperscape.club. Solution: Configure R2 CORS (see R2 CORS Configuration).

R2 CORS Configuration

Automated Configuration

The deploy-cloudflare.yml workflow includes a CORS configuration step:
- name: Configure R2 CORS
  run: |
    wrangler r2 bucket cors set hyperscape-assets \
      --file scripts/r2-cors-config.json
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Manual Configuration

# Run configuration script
bash scripts/configure-r2-cors.sh
Script Contents:
#!/bin/bash
# scripts/configure-r2-cors.sh

wrangler r2 bucket cors set hyperscape-assets \
  --file scripts/r2-cors-config.json
CORS Config File (commit 055779a):
{
  "allowed": {
    "origins": ["*"],
    "methods": ["GET", "HEAD"],
    "headers": ["*"]
  },
  "exposed": ["ETag"],
  "maxAge": 3600
}
Use nested allowed.origins/methods/headers structure. The old flat format (allowedOrigins) causes wrangler to fail.

Verify CORS

# Test CORS headers
curl -I -H "Origin: https://hyperscape.gg" \
  https://assets.hyperscape.club/models/player.glb

# Should include:
# Access-Control-Allow-Origin: *
# Access-Control-Expose-Headers: ETag

CSP Configuration

Content Security Policy

The client includes CSP headers in public/_headers:
Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https://auth.privy.io https://*.privy.io; 
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; 
  img-src 'self' data: https: blob:; 
  font-src 'self' data: https://fonts.gstatic.com; 
  connect-src 'self' wss: https: ws://localhost:* http://localhost:*; 
  frame-src 'self' https://auth.privy.io https://*.privy.io; 
  worker-src 'self' blob:; 
  media-src 'self' blob:;
Recent Updates (Feb 26 2026): Allow data: URLs for WASM (commit 8626299):
  • Required for PhysX WASM loading
  • img-src 'self' data: https: blob:
  • font-src 'self' data: https://fonts.gstatic.com
Allow Google Fonts (commit e012ed2):
  • Required for Rubik font
  • style-src 'self' 'unsafe-inline' https://fonts.googleapis.com
  • font-src 'self' data: https://fonts.gstatic.com
Remove broken report-uri (commit 8626299):
  • report-uri /api/csp-report removed (endpoint didn’t exist)

Vite Configuration

Node Polyfills Fix (commit e012ed2)

Problem: Production builds failed with “Failed to resolve module specifier” errors. Solution: Add aliases to resolve polyfill shims:
// packages/client/vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      'vite-plugin-node-polyfills/shims/buffer': 'vite-plugin-node-polyfills/dist/shims/buffer',
      'vite-plugin-node-polyfills/shims/global': 'vite-plugin-node-polyfills/dist/shims/global',
      'vite-plugin-node-polyfills/shims/process': 'vite-plugin-node-polyfills/dist/shims/process',
    }
  },
  plugins: [
    nodePolyfills({
      protocolImports: false,  // Disable to avoid unresolved imports
    })
  ]
});

Production Checklist

  • CLOUDFLARE_API_TOKEN configured in GitHub Secrets
  • PUBLIC_PRIVY_APP_ID configured in GitHub Secrets
  • R2 CORS configured for asset loading
  • Custom domains configured in Cloudflare Pages dashboard
  • CSP headers configured in public/_headers
  • Environment variables set for production
  • Build succeeds locally with bun run build:client