| 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
| File | Purpose |
|---|
api/src/db/entity/world.ts | World database entity (TypeORM) |
api/src/routes/worlds.ts | All world API endpoints |
frontend/src/actions/main-actions.tsx | World CRUD actions |
frontend/src/components/editor-page.tsx | Editor load/save with adapters |
frontend/src/components/explore-page.jsx | Public worlds listing |
frontend/src/editor/data-migrations.ts | Legacy data format migrations |
frontend/src/helpers/api.ts | HTTP request helper |
Database Model
@Entity({ name: "worlds" })
class World {
id: number;
name: string;
data: string | null;
thumbnail: string;
playCount: number;
forkCount: number;
userId: number;
forkParentId: number;
createdAt: Date;
updatedAt: Date;
}
API Endpoints
Public Endpoints (No Auth)
| Endpoint | Purpose |
|---|
GET /worlds/explore | Top 50 worlds by playCount |
GET /worlds/:id | Fetch world + increment playCount |
Authenticated Endpoints
| Endpoint | Purpose |
|---|
GET /worlds?user=me | List current user's worlds |
POST /worlds | Create new world |
POST /worlds?from=id | Clone from existing world |
POST /worlds?from=id&fork=true | Fork (clone + track parent) |
PUT /worlds/:id | Update name, thumbnail, or data |
DELETE /worlds/:id | Delete world |
Explore Page Query
const worlds = await World.find({
relations: ["user", "forkParent"],
order: { playCount: "DESC" },
take: 50,
});
Create World Logic
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(...))
};
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);
};
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)
const template = from ? await makeRequest(`/worlds/${from}`) : {};
const storageKey = `ls-${Date.now()}`;
localStorage.setItem(storageKey, JSON.stringify({...template, id: storageKey}));
window.location.href = `/editor/${storageKey}?localstorage=true`;
Uploading After Sign-In
const created = await POST /worlds
await PUT /worlds/${created.id} with { name, data, thumbnail }
localStorage.setItem(storageKey, JSON.stringify({ uploadedAsId: created.id }));
window.location.href = `/editor/${created.id}`;
Data Migrations
When loading worlds, applyDataMigrations() handles legacy formats:
action.to → action.value
action.value: string → { constant: string }
conditions: object → conditions: array[]
transform: "90deg" → "90"
transform: "flip-xy" → "180"
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
- Play Count Tracking: Every
GET /worlds/:id increments playCount
- Fork Tracking: Forking increments source's
forkCount and sets forkParentId
- Tutorial World: Special ID "tutorial" maps to
process.env.TUTORIAL_WORLD_ID
- Basic Auth: All authenticated requests use
Authorization: Basic {base64(user:pass)}
- Debounced Saves: Editor saves 5 seconds after last change
- Upload Deduplication: localStorage stores
uploadedAsId to prevent re-uploads