# Architecture

> The whole hosted Moonspell system on one page, from the app and the server to the sandbox, storage, the edge Worker and each game's own backend.

This page explains how the hosted Moonspell product fits together. Players chat with **Nova** in the Moonspell app. Nova starts a **worker run**: Claude Code, headless, in a throwaway sandbox, editing that game's own repo with the open-source engine. The run exports the game, the server stores it as a new version, and the app plays it.

Most of this system is public in this repo: the server, Nova, the run workflow, the edge Worker, the contracts and the feature specs. The native iOS and Android app and the bundling CLI are private. [What is open and what is private](https://docs.moonspell.app/start/open-source.md) draws that line.

## The system on one page

```text
  Moonspell apps                      Server (Next.js on Vercel)                 Sandbox (Daytona), one per run
  --------------                      --------------------------                 ------------------------------
  iOS, Android (private)    chat      Nova (agent/)                              Claude Code, headless
  web (src/web, src/app)  ------->    make_game / change_game                    in a clone of the game's repo
        |                              |                                          |
        |                              v                                          |
        |                             game_runs row (queued)                      |
        |                              |                                          |
        |                              v                                          |
        |                             game-run workflow (src/workflows/)  ------> creates the box, starts run.sh
        |                              |  polls every 15 s  <------------------    exit code, progress line
        |                              |  uploads the exports  <---------------    one export per platform
        |                              |  pushes the commit  ---> GitHub (one private repo per game)
        |                              v
        |                             Postgres (Neon): users, games, builds, runs, credits
        |                             Blob storage (Vercel Blob): every version's files, covers
        |
        |   live progress (WebSocket)        Edge Worker (edge/, Cloudflare)
        +<---------------------------------  RunChannel Durable Object per run  <---- relay in the box posts events
        |                                    proxies every other path to the server
        |
        |   play a game                      Game backend (optional, one Cloudflare Worker per game)
        +--------------------------------->  leaderboards, presence, rooms, saves
```

## The parts

| Part | Where | Runs on | What it does |
|---|---|---|---|
| Moonspell app | private (iOS, Android); `src/web/` and `src/app/(app)/` (web) | phones, browsers | The games feed, Nova's chat, the player, sharing. Downloads a version's export for its platform and runs it. |
| Server | repo root: `src/`, `agent/`, `db/` | Vercel | The app API (`src/app/api/app/`), Nova (mounted at `/eve/v1/*`), the run and cover workflows (`src/workflows/`), the shared-game page `/g/<buildId>`, the web app. |
| Sandbox | created by `src/lib/daytona.ts` | a sandbox provider (Daytona) | One throwaway box per run with Claude Code, git, Node and a copy of the game template. Deleted when the run ends. |
| Edge | `edge/` | Cloudflare Workers | The public host. Proxies the server, streams live run progress through one Durable Object per run, and meters the builder's Claude calls when credits are on. |
| Game template | `templates/crescent-game/` | copied into every game repo | The engine, the starter game and the instructions and skills the worker reads. See [Engine overview](https://docs.moonspell.app/engine/overview.md). |
| Game backend | the template's `realtime-backend` skill | each game's own Cloudflare Worker | Live leaderboards, "N playing now", rooms and synced saves. See [Backend overview](https://docs.moonspell.app/backend/overview.md). |

`next.config.ts` wraps the Next.js app with `withEve(withWorkflow(...))`, so Nova and the workflows deploy with the server as one project.

## One request, start to finish

1. **Chat.** The player writes to Nova. Nova asks a question or two at most, then calls `make_game` (a new game) or `change_game` (an existing one) with a complete brief.
2. **Queue.** The tool inserts a `queued` row in `game_runs` and asks the server to start the run (`POST /api/internal/runs`). A game has at most one active run at a time on each branch.
3. **Build.** The workflow creates a sandbox, writes the run's files into it and starts the run script. Claude Code builds in checkpoints, so a run always has a playable version to fall back on. See [Nova and the worker](https://docs.moonspell.app/moonspell/nova-and-the-worker.md).
4. **Watch.** A relay inside the box posts progress lines to the edge, and the app shows them on a building card. See [Live progress](https://docs.moonspell.app/moonspell/realtime.md).
5. **Store.** The workflow uploads each platform's export to blob storage, records the version in `game_builds` and pushes the commit to the game's repo. See [Builds and versions](https://docs.moonspell.app/moonspell/builds.md).
6. **Tell Nova.** The workflow posts a hidden `[Build ready]` or `[Build failed]` message into the chat. Nova answers with a card that plays the new version.
7. **Share.** Every version has its own link, `/g/<buildId>`. See [Sharing](https://docs.moonspell.app/moonspell/sharing.md).

## Server code map

| Area | Files |
|---|---|
| Nova | `agent/agent.ts`, `agent/instructions.ts`, `agent/instructions/status.ts`, `agent/tools/`, `agent/lib/`, `agent/channels/app.ts`, `agent/hooks/meter.ts` |
| Runs | `src/workflows/game-run.ts` (the workflow), `src/workflows/game-run-steps.ts` (its steps) |
| Sandbox and run script | `src/lib/daytona.ts` |
| Games, versions, runs | `src/lib/games.ts` |
| Game repos | `src/lib/github.ts` (a GitHub App creates one private repo per game and mints single-repo tokens) |
| Live progress | `src/lib/realtime.ts` (signs `crt1` tokens, publishes events to the edge) |
| Game backends | `src/lib/cloudflare-gateway.ts` (signs tokens a Cloudflare gateway accepts for one game's resources) |
| Accounts | `src/lib/app-session.ts` (anonymous accounts, bearer sessions), `src/lib/sign-in.ts` (Google and Apple) |
| Which export each app gets | `src/lib/client.ts` |
| Sharing | `src/lib/app-clip.ts`, `src/app/g/[buildId]/`, `src/app/.well-known/` |
| Community builds | `src/lib/community.ts`, `community-merge.ts`, `community-cancel.ts`, `judge.ts`, `badges.ts` |
| Credits | `src/lib/credit-prices.ts`, `src/lib/credits.ts`, `src/lib/usage.ts` |
| Crash fixes | `src/lib/heal.ts` |
| Generated art, sounds and models | `src/lib/higgsfield.ts`, `src/lib/models.ts`, `src/lib/sprite-sheet.ts`, `src/workflows/game-cover.ts` |
| Server-to-server calls | `src/lib/internal.ts` (`INTERNAL_API_SECRET`) |

Two base URLs matter (`src/lib/users.ts`): `PUBLIC_BASE_URL` is the public host for everything players see, and `CRESCENT_APP_URL` is where server code reaches the Next.js app directly, never through the edge.

## Accounts

Every app starts on an anonymous account (`POST /api/app/account`), so playing needs nothing. Making or changing a game needs a registered account: Google on every app, Apple on iOS (`POST /api/app/google`, `POST /api/app/apple`). Signing in links the anonymous account, or switches to the account that identity already has. `CRESCENT_REQUIRE_SIGN_IN` turns the rule on or off; unset, it is on once `GOOGLE_WEB_CLIENT_ID` is set (`signInRequired()` in `src/lib/sign-in.ts`).

## The API

The app API lives under `/api/app/*`. It answers JSON with `Cache-Control: no-store`, and most routes need `Authorization: Bearer <session token>`.

| Routes | What they cover |
|---|---|
| `/api/app/account`, `/me`, `/google`, `/apple`, `/config` | Accounts and sign-in |
| `/api/app/games`, `/games/:id`, `/feed`, `/previews/:buildId` | The player's games, the public feed, one shared version |
| `/api/app/games/:id/play`, `/games/:id/crash` | Play time, crash reports |
| `/api/app/games/:id/merge`, `/games/:id/community`, `/contributions/:id/cancel`, `/contributions/:id/badges` | [Community builds](https://docs.moonspell.app/moonspell/community-builds.md) |
| `/api/app/runs/:id/assets`, `/credits`, `/usage` | [Credits](https://docs.moonspell.app/moonspell/credits.md) |
| `/api/app/chats`, `/files` | Chat list, picture uploads |
| `/eve/v1/app/chats/:chatId/messages`, `/eve/v1/app/sessions/:sessionId/stream`, `/eve/v1/app/chats/:chatId/cancel`, `/eve/v1/app/chats/:chatId/build` | Nova's chat channel (`agent/channels/app.ts`); the stream is NDJSON |
| `/api/internal/runs`, `/api/internal/meter/*`, `/eve/v1/app/internal/deliver` | Server-to-server, with `INTERNAL_API_SECRET` |

Every app sends `X-Crescent-Client: ios`, `android` or `web`, and the server hands each one its own export of a version. See [Three apps, one product](https://docs.moonspell.app/moonspell/three-apps.md).

## Data

Postgres, with the schema in `db/migrations/` (applied by `npm run db:migrate`, which records each file in `schema_migrations`).

| Table | Holds |
|---|---|
| `users` | A player: anonymous until a Google or Apple identity is linked; credit balance |
| `games` | One game, one repo: title, slug, repo name, cover |
| `app_chats` | One conversation with Nova, tied to a game; `contributing` when it is about someone else's game |
| `game_builds` | One playable version: version number, commit, one manifest URL per platform, notes, design document. Its random id is the share link. Drafts carry a branch and a draft number instead of a version |
| `game_runs` | One worker run: request, status, progress line, result, error, the version it made |
| `game_contributions`, `contribution_badges` | Accepted or declined community changes, and badges |
| `credit_ledger` | Every credit change |
| `game_plays`, `game_playtests` | Play time, and the results of scripted playtests |
| `service_tokens` | Refreshed credentials for server-side services |

A partial unique index on `game_runs` allows at most one queued or running run per game and branch: one on the game's public branch, and one on each player's draft branch.

## Storage

| What | Where |
|---|---|
| Accounts, games, chats, runs, versions, credits | Postgres (`DATABASE_URL`) |
| Every version's exported files, covers, chat pictures | Vercel Blob (`BLOB_READ_WRITE_TOKEN`), under `games/<gameId>/...` and `chat/<userId>/...` |
| Game source | GitHub, one private repo per game in `GAMES_GITHUB_ORG` |
| Nova's conversations | eve's durable sessions |
| Live run events | The edge's `RunChannel` Durable Object for that run |
| Game data (scores, rooms, saves) | The game's own Cloudflare Worker, when it has a backend |
