Game I-frame Architecture

How arcade loads games into iframes, resolves S3 URLs, and communicates with games.

Overview

graph TB subgraph "User Browser" A["/play/grid-ranger"] --> B["+page.server.js"] B --> C["+page.svelte"] C --> D["iframe src=gameData.src"] end subgraph "Arcade Server" B --> E["gamepak.js"] E --> F["Gamepak.forOrg()"] D --> G["/api/game-files/..."] G --> H["S3Provider"] end subgraph "Configuration" F --> I["tetra.toml"] F --> J["secrets.env"] F --> K["games.json (S3)"] end subgraph "S3 Storage" H --> L["pja-games bucket"] K -.-> L end L --> D

Game URL Generation Flow

Step 1: Page Load

sequenceDiagram participant Browser participant PageServer as +page.server.js participant Gamepak as gamepak.js participant OrgConfig participant S3 Browser->>PageServer: GET /play/grid-ranger PageServer->>Gamepak: getGame("grid-ranger") Gamepak->>OrgConfig: load("pixeljam-arcade") OrgConfig-->>Gamepak: { bucket, endpoint, credentials } Gamepak->>S3: GET games.json S3-->>Gamepak: manifest with game configs Gamepak-->>PageServer: game object PageServer-->>Browser: { game, games }

Step 2: Configuration Resolution

flowchart LR subgraph "~/tetra/orgs/pixeljam-arcade/" A["tetra.toml"] --> C["OrgConfig"] B["secrets.env"] --> C end C --> D["Gamepak Instance"] D --> E["bucket: pja-games"] D --> F["endpoint: sfo3.digitaloceanspaces.com"] D --> G["credentials: { key, secret }"]

tetra.toml provides:

[games]
bucket = "pja-games"
endpoint = "https://sfo3.digitaloceanspaces.com"

[games.s3]
access_key = "$DO_SPACES_KEY"
secret_key = "$DO_SPACES_SECRET"

secrets.env provides:

DO_SPACES_KEY=actual-key-here
DO_SPACES_SECRET=actual-secret-here

Step 3: Manifest Structure

The games.json file in S3 is the source of truth for game metadata:

classDiagram class GamesManifest { _config: AccessConfig games: Map~string, Game~ } class Game { slug: string name: string src: string url_path: string url_path_demo: string url_path_dev: string engine: Engine access_control: AccessControl } class Engine { path: string path_demo: string path_dev: string version: string } class AccessControl { requires_auth: boolean min_role: string min_subscription: string } GamesManifest --> Game Game --> Engine Game --> AccessControl

Example games.json:

{
  "_config": {
    "access_strategies": {
      "role_based": { "hierarchy": { "guest": 0, "user": 1, "dev": 2, "admin": 3 } }
    }
  },
  "games": {
    "grid-ranger": {
      "slug": "grid-ranger",
      "name": "Grid Ranger",
      "src": "/api/game-files/grid-ranger/v1.0.0/index.html",
      "url_path": "grid-ranger/v1.0.0/index.html",
      "url_path_demo": "grid-ranger/demo/index.html",
      "engine": {
        "path": "grid-ranger/v1.0.0/index.html",
        "version": "1.0.0"
      }
    }
  }
}

Step 4: URL Resolution & Variant Selection

flowchart TD A["User requests game"] --> B{"User role?"} B -->|"admin/dev"| C["Check dev variant"] B -->|"user/guest"| D["Check default variant"] C --> E{"path_dev exists?"} E -->|Yes| F["Use path_dev"] E -->|No| D D --> G{"path exists?"} G -->|Yes| H["Use path"] G -->|No| I["Use path_demo"] F --> J["Build URL: /api/game-files/{path}"] H --> J I --> J

Step 5: S3 Proxy

sequenceDiagram participant Iframe participant GameFilesAPI as /api/game-files/ participant Gamepak participant S3 Iframe->>GameFilesAPI: GET /api/game-files/grid-ranger/v1.0.0/index.html GameFilesAPI->>GameFilesAPI: Extract slug: "grid-ranger" GameFilesAPI->>GameFilesAPI: Check referrer (security) alt HTML file (entry point) GameFilesAPI->>Gamepak: canAccessGame(slug, user) Gamepak-->>GameFilesAPI: { allowed: true } end GameFilesAPI->>Gamepak: getFileStream(path) Gamepak->>S3: GetObject S3-->>Gamepak: Stream + ContentType Gamepak-->>GameFilesAPI: { body, contentType } GameFilesAPI-->>Iframe: Response with CORS headers

S3 Bucket Structure

graph TD subgraph "pja-games bucket" A["games.json"] subgraph "grid-ranger/" B["v1.0.0/"] C["v1.1.0/"] D["demo/"] B --> B1["index.html"] B --> B2["game.js"] B --> B3["assets/"] D --> D1["index.html"] end subgraph "quadrapong/" E["v2.0.0/"] E --> E1["index.html"] end end

Iframe Communication (PJA-SDK)

sequenceDiagram participant Arcade as Arcade (+page.svelte) participant Iframe as Game Iframe participant SDK as PJA-SDK (in game) Note over Arcade,SDK: Game loads and initializes SDK->>Arcade: postMessage({ type: 'client:loaded', source: 'pja-sdk' }) Arcade->>Iframe: postMessage({ type: 'container-size', width, height, source: 'pja-host' }) Note over Arcade,SDK: Gameplay SDK->>Arcade: { type: 'game:start' } SDK->>Arcade: { type: 'game:score', scores: [100, 0, 0, 0] } SDK->>Arcade: { type: 'game:state', state: 'playing' } Note over Arcade,SDK: Game ends SDK->>Arcade: { type: 'game:end', scores: [12500, 0, 0, 0] }

Message Types

classDiagram class HostToGame { +container-size(width, height) +game:control(action: start|stop|pause|resume) +audio:volume(volume: 0-1) +audio:mute(muted: boolean) } class GameToHost { +client:loaded() +game:start() +game:stop() +game:pause() +game:resume() +game:end(scores) +game:score(scores) +game:state(state) +game:paddle(player, value) }

High Scores Integration

Architecture

flowchart TB subgraph "Arcade" A["Game Iframe"] -->|"postMessage"| B["+page.svelte"] B -->|"fire-and-forget"| C["fetch()"] end subgraph "Pbase" C -->|"POST /api/scores"| D["Scores API"] D --> E["scores/{gameId}/"] D --> F["leaderboard/{gameId}.json"] end subgraph "Storage" E --> G["~/tetra/orgs/pixeljam-arcade/pdata/"] F --> G end H["Game requests leaderboard"] -->|"GET /api/scores/{gameId}"| D

Scores API Flow

sequenceDiagram participant Game participant Arcade participant Pbase participant Storage Note over Game,Arcade: Game ends Game->>Arcade: { type: 'game:end', score: 12500 } Arcade->>Pbase: POST /api/scores { gameId, score, userId? } alt Success Pbase->>Storage: Write score record Pbase->>Storage: Update leaderboard Pbase-->>Arcade: 201 { id, rank } else Failure (silent) Pbase-->>Arcade: Error (ignored) end Note over Game,Arcade: User views leaderboard Game->>Arcade: { type: 'scores:get' } Arcade->>Pbase: GET /api/scores/grid-ranger Pbase->>Storage: Read leaderboard Storage-->>Pbase: Top 100 scores Pbase-->>Arcade: { scores, myBest, rank } Arcade->>Game: { type: 'scores:data', scores }

Data Models

erDiagram SCORE { string id PK string gameId FK int score string userId string playerName datetime timestamp json meta } LEADERBOARD { string gameId PK json top100 int totalScores datetime lastUpdated } GAME { string slug PK string name string url_path } GAME ||--o{ SCORE : has GAME ||--|| LEADERBOARD : has

API Specification

classDiagram class ScoresAPI { +POST /api/scores +GET /api/scores/:gameId +GET /api/scores/:gameId/my +DELETE /api/scores/:id (admin) } class PostScoreRequest { gameId: string score: number userId?: string playerName?: string meta?: object } class PostScoreResponse { id: string rank: number } class GetScoresResponse { gameId: string scores: Score[] total: number myBest?: Score } class Score { rank: number score: number playerName: string date: string } ScoresAPI --> PostScoreRequest ScoresAPI --> PostScoreResponse ScoresAPI --> GetScoresResponse GetScoresResponse --> Score

Entry Point Management via Pbase

flowchart LR subgraph "Pbase Admin" A["Games Config UI"] --> B["PATCH /api/games/:slug"] end subgraph "Pbase Server" B --> C["Update games.json"] end subgraph "S3" C --> D["pja-games/games.json"] end subgraph "Arcade" E["Gamepak.loadManifest()"] --> D E --> F["Game loads with new entry point"] end

Key benefit: Arcade code never changes. To point a game at a different entry file:

  1. Pbase admin UI: Change url_path for the game
  2. Pbase updates games.json in S3
  3. Next arcade page load picks up new path

Port Configuration

Service local dev staging prod
arcade 8400 8400 8401 8400
cabinet 5173 5173 5174 5173
pbase 2600 2600 2600 2600

Staging uses +1 offset since it may share a machine with dev or prod.

File Locations

Component Path
Org config ~/tetra/orgs/pixeljam-arcade/tetra.toml
Secrets ~/tetra/orgs/pixeljam-arcade/secrets.env
Gamepak module ~/src/devops/tetra/bash/gamepak/
Arcade gamepak wrapper arcade/src/lib/server/gamepak.js
Game files API arcade/src/routes/api/game-files/[...path]/+server.js
Play page arcade/src/routes/play/[game]/+page.svelte
Games manifest S3: pja-games/games.json