Skip to main content

Architecture

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 draws that line.

The system on one page​

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​

PartWhereRuns onWhat it does
Moonspell appprivate (iOS, Android); src/web/ and src/app/(app)/ (web)phones, browsersThe games feed, Nova's chat, the player, sharing. Downloads a version's export for its platform and runs it.
Serverrepo root: src/, agent/, db/VercelThe 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.
Sandboxcreated by src/lib/daytona.tsa 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.
Edgeedge/Cloudflare WorkersThe 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 templatetemplates/crescent-game/copied into every game repoThe engine, the starter game and the instructions and skills the worker reads. See Engine overview.
Game backendthe template's realtime-backend skilleach game's own Cloudflare WorkerLive leaderboards, "N playing now", rooms and synced saves. See Backend overview.

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.
  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.
  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.
  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.

Server code map​

AreaFiles
Novaagent/agent.ts, agent/instructions.ts, agent/instructions/status.ts, agent/tools/, agent/lib/, agent/channels/app.ts, agent/hooks/meter.ts
Runssrc/workflows/game-run.ts (the workflow), src/workflows/game-run-steps.ts (its steps)
Sandbox and run scriptsrc/lib/daytona.ts
Games, versions, runssrc/lib/games.ts
Game repossrc/lib/github.ts (a GitHub App creates one private repo per game and mints single-repo tokens)
Live progresssrc/lib/realtime.ts (signs crt1 tokens, publishes events to the edge)
Game backendssrc/lib/cloudflare-gateway.ts (signs tokens a Cloudflare gateway accepts for one game's resources)
Accountssrc/lib/app-session.ts (anonymous accounts, bearer sessions), src/lib/sign-in.ts (Google and Apple)
Which export each app getssrc/lib/client.ts
Sharingsrc/lib/app-clip.ts, src/app/g/[buildId]/, src/app/.well-known/
Community buildssrc/lib/community.ts, community-merge.ts, community-cancel.ts, judge.ts, badges.ts
Creditssrc/lib/credit-prices.ts, src/lib/credits.ts, src/lib/usage.ts
Crash fixessrc/lib/heal.ts
Generated art, sounds and modelssrc/lib/higgsfield.ts, src/lib/models.ts, src/lib/sprite-sheet.ts, src/workflows/game-cover.ts
Server-to-server callssrc/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>.

RoutesWhat they cover
/api/app/account, /me, /google, /apple, /configAccounts and sign-in
/api/app/games, /games/:id, /feed, /previews/:buildIdThe player's games, the public feed, one shared version
/api/app/games/:id/play, /games/:id/crashPlay time, crash reports
/api/app/games/:id/merge, /games/:id/community, /contributions/:id/cancel, /contributions/:id/badgesCommunity builds
/api/app/runs/:id/assets, /credits, /usageCredits
/api/app/chats, /filesChat 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/buildNova's chat channel (agent/channels/app.ts); the stream is NDJSON
/api/internal/runs, /api/internal/meter/*, /eve/v1/app/internal/deliverServer-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.

Data​

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

TableHolds
usersA player: anonymous until a Google or Apple identity is linked; credit balance
gamesOne game, one repo: title, slug, repo name, cover
app_chatsOne conversation with Nova, tied to a game; contributing when it is about someone else's game
game_buildsOne 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_runsOne worker run: request, status, progress line, result, error, the version it made
game_contributions, contribution_badgesAccepted or declined community changes, and badges
credit_ledgerEvery credit change
game_plays, game_playtestsPlay time, and the results of scripted playtests
service_tokensRefreshed 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​

WhatWhere
Accounts, games, chats, runs, versions, creditsPostgres (DATABASE_URL)
Every version's exported files, covers, chat picturesVercel Blob (BLOB_READ_WRITE_TOKEN), under games/<gameId>/... and chat/<userId>/...
Game sourceGitHub, one private repo per game in GAMES_GITHUB_ORG
Nova's conversationseve's durable sessions
Live run eventsThe edge's RunChannel Durable Object for that run
Game data (scores, rooms, saves)The game's own Cloudflare Worker, when it has a backend