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:
- Pbase admin UI: Change
url_path for the game
- Pbase updates
games.json in S3
- 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 |