world-lifecycle

Provides context about how worlds (games) are created, loaded, saved, shared, and fetched in Codako. Use when working on API routes for worlds, the explore page, editor save/load logic, forking, or localStorage handling for anonymous users.

$ Installieren

git clone https://github.com/Foundry376/ghkids /tmp/ghkids && cp -r /tmp/ghkids/.claude/skills/world-lifecycle ~/.claude/skills/ghkids

// tip: Run this command in your terminal to install the skill


name: world-lifecycle description: Provides context about how worlds (games) are created, loaded, saved, shared, and fetched in Codako. Use when working on API routes for worlds, the explore page, editor save/load logic, forking, or localStorage handling for anonymous users.

World Lifecycle & Persistence

When to Use This Skill

Activate this skill when working on:

  • API routes for worlds (api/src/routes/worlds.ts)
  • The World database entity (api/src/db/entity/world.ts)
  • The Explore page (frontend/src/components/explore-page.jsx)
  • Editor page load/save logic (frontend/src/components/editor-page.tsx)
  • World creation, forking, or cloning (frontend/src/actions/main-actions.tsx)
  • Data migrations for older world formats (frontend/src/editor/data-migrations.ts)
  • Anonymous user localStorage handling
  • API helper functions (frontend/src/helpers/api.ts)

Key Files

FilePurpose
api/src/db/entity/world.tsWorld database entity (TypeORM)
api/src/routes/worlds.tsAll world API endpoints
frontend/src/actions/main-actions.tsxWorld CRUD actions
frontend/src/components/editor-page.tsxEditor load/save with adapters
frontend/src/components/explore-page.jsxPublic worlds listing
frontend/src/editor/data-migrations.tsLegacy data format migrations
frontend/src/helpers/api.tsHTTP request helper

Database Model

@Entity({ name: "worlds" })
class World {
  id: number;              // Primary key
  name: string;            // World name (default: "Untitled")
  data: string | null;     // JSON-serialized game state
  thumbnail: string;       // Preview image (base64 or URL)
  playCount: number;       // Incremented on each load
  forkCount: number;       // Incremented when forked
  userId: number;          // Owner's user ID
  forkParentId: number;    // Reference to original (if forked)
  createdAt: Date;
  updatedAt: Date;
}

API Endpoints

Public Endpoints (No Auth)

EndpointPurpose
GET /worlds/exploreTop 50 worlds by playCount
GET /worlds/:idFetch world + increment playCount

Authenticated Endpoints

EndpointPurpose
GET /worlds?user=meList current user's worlds
POST /worldsCreate new world
POST /worlds?from=idClone from existing world
POST /worlds?from=id&fork=trueFork (clone + track parent)
PUT /worlds/:idUpdate name, thumbnail, or data
DELETE /worlds/:idDelete world

Explore Page Query

// Returns top 50 worlds sorted by popularity
const worlds = await World.find({
  relations: ["user", "forkParent"],
  order: { playCount: "DESC" },
  take: 50,
});

Create World Logic

// POST /worlds?from=<id>&fork=<true>
if (sourceWorld) {
  if (fork) sourceWorld.forkCount += 1;
  newWorld = {
    userId: req.user.id,
    name: sourceWorld.name,
    data: sourceWorld.data,
    thumbnail: sourceWorld.thumbnail,
    forkParentId: fork ? sourceWorld.id : null,
  };
} else {
  newWorld = { userId: req.user.id, name: "Untitled", data: null, thumbnail: "#" };
}

Frontend Architecture

Two Storage Adapters

The editor uses different adapters based on authentication:

const APIAdapter = {
  load: (me, worldId) => GET /worlds/:id,
  save: (me, worldId, json) => PUT /worlds/:id
};

const LocalStorageAdapter = {
  load: (me, worldId) => localStorage.getItem(worldId),
  save: (me, worldId, json) => localStorage.setItem(worldId, JSON.stringify(...))
};

// Selection based on URL
const Adapter = window.location.href.includes("localstorage")
  ? LocalStorageAdapter
  : APIAdapter;

Auto-Save Mechanism

const saveWorldSoon = () => {
  if (_saveTimeout.current) clearTimeout(_saveTimeout.current);
  _saveTimeout.current = setTimeout(() => saveWorld(), 5000);
};

// Called on every world change via StoreProvider.onWorldChanged

Before-Unload Protection

window.addEventListener("beforeunload", () => {
  if (_saveTimeout.current) {
    saveWorld();
    return "Your changes are still saving...";
  }
});

Anonymous User Flow

Creating a World (Not Logged In)

// 1. Fetch source world (if cloning)
const template = from ? await makeRequest(`/worlds/${from}`) : {};

// 2. Generate localStorage key
const storageKey = `ls-${Date.now()}`;

// 3. Store in localStorage
localStorage.setItem(storageKey, JSON.stringify({...template, id: storageKey}));

// 4. Redirect with localstorage flag
window.location.href = `/editor/${storageKey}?localstorage=true`;

Uploading After Sign-In

// 1. Create empty world on server
const created = await POST /worlds

// 2. Upload localStorage data
await PUT /worlds/${created.id} with { name, data, thumbnail }

// 3. Mark localStorage as uploaded (prevents duplicates)
localStorage.setItem(storageKey, JSON.stringify({ uploadedAsId: created.id }));

// 4. Redirect to real world
window.location.href = `/editor/${created.id}`;

Data Migrations

When loading worlds, applyDataMigrations() handles legacy formats:

// Key transformations:
action.to โ†’ action.value                    // Renamed field
action.value: string โ†’ { constant: string } // Wrapped in RuleValue
conditions: object โ†’ conditions: array[]    // Object to array format
transform: "90deg" โ†’ "90"                   // Remove "deg" suffix
transform: "flip-xy" โ†’ "180"                // Normalize flip

Migrations run automatically on load, allowing old saved worlds to work with current engine.

Lifecycle Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      EXPLORE PAGE                           โ”‚
โ”‚  GET /worlds/explore โ†’ Top 50 by playCount                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      PLAY/VIEW                              โ”‚
โ”‚  GET /worlds/:id โ†’ playCount++, return full data            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ                               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   CREATE (Logged In)     โ”‚    โ”‚   CREATE (Anonymous)     โ”‚
โ”‚   POST /worlds?from&fork โ”‚    โ”‚   Store in localStorage  โ”‚
โ”‚   โ†’ New DB record        โ”‚    โ”‚   โ†’ ls-{timestamp} key   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚                               โ”‚
              โ–ผ                               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                       EDITOR                                โ”‚
โ”‚  Load: APIAdapter or LocalStorageAdapter                    โ”‚
โ”‚  Auto-save: PUT /worlds/:id (debounced 5s)                  โ”‚
โ”‚  Data migrations applied on load                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚
              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   UPLOAD (Anonymous โ†’ Logged In)            โ”‚
โ”‚  POST /worlds (create) โ†’ PUT /worlds/:id (upload data)      โ”‚
โ”‚  localStorage marked with uploadedAsId                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Important Patterns

  1. Play Count Tracking: Every GET /worlds/:id increments playCount
  2. Fork Tracking: Forking increments source's forkCount and sets forkParentId
  3. Tutorial World: Special ID "tutorial" maps to process.env.TUTORIAL_WORLD_ID
  4. Basic Auth: All authenticated requests use Authorization: Basic {base64(user:pass)}
  5. Debounced Saves: Editor saves 5 seconds after last change
  6. Upload Deduplication: localStorage stores uploadedAsId to prevent re-uploads