Skip to main content

Cloudflare Pages Deployment

This guide covers deploying the Hyperscape web client to Cloudflare Pages with automatic builds and R2 asset hosting.

Overview

The Cloudflare deployment consists of:
  • Client: Static site hosted on Cloudflare Pages
  • Assets: 3D models, textures, audio hosted on Cloudflare R2
  • CORS: Configured for cross-origin asset loading

Architecture

┌──────────────────────────────────────────────────────────┐
│ Cloudflare Pages (hyperscape.gg)                         │
│  - Static HTML/CSS/JS                                    │
│  - Vite production build                                 │
│  - Connects to Railway game server via WebSocket        │
└──────────────────────────────────────────────────────────┘

                          ├─ WebSocket → wss://hyperscape-production.up.railway.app/ws
                          ├─ HTTP API → https://hyperscape-production.up.railway.app
                          └─ Assets → https://assets.hyperscape.club (R2)

┌──────────────────────────────────────────────────────────┐
│ Cloudflare R2 (assets.hyperscape.club)                   │
│  - 3D models (.glb, .gltf)                               │
│  - Textures (.png, .jpg, .ktx2)                          │
│  - Audio (.mp3, .ogg)                                    │
│  - Manifests (.json)                                     │
└──────────────────────────────────────────────────────────┘

Prerequisites

Cloudflare Account

  1. Sign up at Cloudflare
  2. Create a Pages project named hyperscape
  3. Create an R2 bucket named hyperscape-assets

GitHub Secrets

Configure in repository settings (Settings → Secrets and variables → Actions):
SecretDescriptionWhere to Find
CLOUDFLARE_API_TOKENAPI token with Pages and R2 permissionsCloudflare Dashboard → My Profile → API Tokens
PUBLIC_PRIVY_APP_IDPrivy app ID (optional, has default)Privy Dashboard

Cloudflare API Token Permissions

Create a token with these permissions:
  • AccountCloudflare PagesEdit
  • AccountR2Edit

Automatic Deployment

Workflow Trigger

The client deploys automatically on push to main when these paths change:
  • packages/client/**
  • packages/shared/** (contains packet definitions)
  • package.json
  • bun.lockb

Workflow File

.github/workflows/deploy-pages.yml

Build Process

  1. Checkout code with submodules
  2. Setup Bun (latest version)
  3. Install dependencies (bun install --frozen-lockfile)
  4. Build client (bun run build:client):
    • Builds packages/shared first (via Turbo)
    • Builds packages/physx-js-webidl (WASM bindings)
    • Builds packages/client (Vite production build)
  5. Deploy to Pages using Wrangler:
    • Project: hyperscape
    • Branch: main
    • Commit hash and message included

Build Environment Variables

Set during build (in workflow):
PUBLIC_PRIVY_APP_ID: cmgk4zu56005kjj0bcaae0rei  # Default
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

Manual Deployment

From Local Machine

# Build client
bun run build:client

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

From GitHub Actions

  1. Go to ActionsDeploy Client to Cloudflare Pages
  2. Click Run workflow
  3. Select main branch
  4. Choose environment: production or preview
  5. Click Run workflow

R2 Asset Hosting

Bucket Setup

  1. Create R2 bucket:
    • Name: hyperscape-assets
    • Location: Automatic (Cloudflare chooses optimal location)
  2. Configure custom domain:
    • Go to R2 bucket → Settings → Custom Domains
    • Add assets.hyperscape.club
    • Create DNS record as instructed by Cloudflare
  3. Configure CORS:
    # Run from repository root
    bash scripts/configure-r2-cors.sh
    
    Or manually via Wrangler:
    wrangler r2 bucket cors set hyperscape-assets \
      --cors-config='{
        "allowed": {
          "origins": ["*"],
          "methods": ["GET", "HEAD"],
          "headers": ["*"]
        },
        "exposed": ["ETag"],
        "maxAge": 3600
      }'
    

Upload Assets

Assets are uploaded separately (not part of client deployment):
# Upload all assets to R2
bun run scripts/sync-r2-assets.mjs
This uploads:
  • packages/server/world/assets/ → R2 bucket
  • Preserves directory structure
  • Skips unchanged files (checksum comparison)

Custom Domains

Primary Domain (hyperscape.gg)

  1. In Cloudflare Pages:
    • Go to project → Settings → Custom domains
    • Add hyperscape.gg
    • Add www.hyperscape.gg (optional redirect)
  2. DNS Configuration:
    • Cloudflare will show required DNS records
    • If using Cloudflare DNS, records are added automatically
    • If using external DNS, create CNAME records as shown
  3. SSL/TLS:
    • Cloudflare automatically provisions SSL certificates
    • Wait for certificate status to become “Active”

Asset Domain (assets.hyperscape.club)

  1. In R2 bucket:
    • Go to Settings → Custom Domains
    • Add assets.hyperscape.club
  2. DNS Configuration:
    • Create CNAME record: assets.hyperscape.club<bucket-id>.r2.cloudflarestorage.com
    • Cloudflare will show the exact target

CORS Configuration

Why CORS is Needed

The client (hyperscape.gg) loads assets from a different origin (assets.hyperscape.club), requiring CORS headers.

R2 CORS Configuration

The scripts/configure-r2-cors.sh script configures:
{
  "allowed": {
    "origins": ["*"],
    "methods": ["GET", "HEAD"],
    "headers": ["*"]
  },
  "exposed": ["ETag"],
  "maxAge": 3600
}
Allowed origins: * (all origins) - safe for public read-only assets Allowed methods: GET, HEAD - read-only access Exposed headers: ETag - for cache validation Max age: 3600 seconds (1 hour) - browser caches CORS preflight

Verify CORS

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

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

Environment Variables

Build-Time Variables

Set in .github/workflows/deploy-pages.yml:
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
These are baked into the build and cannot be changed at runtime.

Runtime Variables

Cloudflare Pages does not support runtime environment variables for static sites. All configuration must be set at build time.

Deployment URLs

Production

Preview

Each commit gets a preview URL:
  • Format: https://<commit-hash>.hyperscape.pages.dev
  • Example: https://50f1a285aa6782ead0066d21616d98a238ea1ae3.hyperscape.pages.dev
Preview deployments use the same build configuration as production.

Troubleshooting

Assets not loading (404 errors)

Symptom: Console errors like Failed to load resource: https://assets.hyperscape.club/models/player/human.glb Causes:
  1. Assets not uploaded to R2
  2. CORS not configured
  3. Custom domain not set up
Fix:
# 1. Upload assets
bun run scripts/sync-r2-assets.mjs

# 2. Configure CORS
bash scripts/configure-r2-cors.sh

# 3. Verify custom domain
curl -I https://assets.hyperscape.club/models/player/human.glb

CORS errors

Symptom: Console error Access to fetch at 'https://assets.hyperscape.club/...' from origin 'https://hyperscape.gg' has been blocked by CORS policy Fix:
# Reconfigure CORS
bash scripts/configure-r2-cors.sh

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

Build failures

Symptom: GitHub Actions workflow fails during build Common causes:
  1. TypeScript errors
  2. Missing dependencies
  3. Out of memory
Fix:
# Test build locally
bun run build:client

# Check for TypeScript errors
bun run typecheck

# Increase Node memory (already set in workflow)
NODE_OPTIONS='--max-old-space-size=4096' bun run build:client

WebSocket connection failures

Symptom: Client cannot connect to game server Causes:
  1. PUBLIC_WS_URL pointing to wrong server
  2. Railway server not running
  3. CORS/Origin validation failing
Fix:
  1. Verify PUBLIC_WS_URL in workflow matches Railway server
  2. Check Railway deployment status
  3. Verify server allows hyperscape.gg origin (see docs/csrf-cross-origin.md)

Advanced Configuration

Multiple Environments

To deploy to staging/preview environments:
  1. Create separate Pages project: hyperscape-staging
  2. Update workflow to deploy to different project based on branch:
    - name: Deploy to Pages
      run: |
        PROJECT_NAME=${{ github.ref == 'refs/heads/main' && 'hyperscape' || 'hyperscape-staging' }}
        npx wrangler pages deploy dist --project-name=$PROJECT_NAME
    

Custom Build Configuration

Edit packages/client/vite.config.ts to customize:
  • Output directory
  • Asset optimization
  • Code splitting
  • Source maps

Caching Strategy

Cloudflare Pages automatically caches:
  • HTML: No cache (always fresh)
  • JS/CSS: Immutable (hashed filenames)
  • Assets: Long cache (1 year)
Configure via packages/client/public/_headers:
/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

/assets/*
  Cache-Control: public, max-age=31536000, immutable

Monitoring

Deployment Status

Check deployment status in:
  • GitHub Actions: Actions tab → Deploy Client to Cloudflare Pages
  • Cloudflare Dashboard: Pages → hyperscape → Deployments

Analytics

Cloudflare Pages provides:
  • Web Analytics: Visitor stats, page views, bandwidth
  • Real User Monitoring: Performance metrics, Core Web Vitals
  • Error tracking: JavaScript errors, failed requests
Access via: Cloudflare Dashboard → Pages → hyperscape → Analytics

Logs

View deployment logs:
  • GitHub Actions: Click on workflow run → View logs
  • Cloudflare: Pages → hyperscape → Deployments → View build log