Three apps, one product
Moonspell is three apps that share no UI code: iOS (SwiftUI), Android (Compose) and web (React). The web app is public, in src/web/ (client code) and src/app/(app)/ (its routes). The iOS and Android apps are private. They stay the same product because they follow the same three things:
- Contracts (
contracts/): names, colours, metrics and every shared sentence, generated into each app. - One server API: the same routes, sessions and shapes for every app (see Architecture).
X-Crescent-Client: ios | android | webpicks each version's export (see Builds and versions). - Feature specs (
spec/features/): what each feature does, one numbered behaviour per row, with a column per app.
Games are not part of this: a game is one source, exported once per platform, and what differs per platform belongs in the engine. See Platform layer.
Contracts
| File | Holds |
|---|---|
contracts/identity.json | The product's names, the share host and URL scheme |
contracts/tokens.json | The look: colours, corner radii, the gutter, layout limits (the web app's one column is capped at layout.maxWidth, 1024) |
contracts/copy.json | Every sentence the apps share, by key; {name} in a string marks a value filled in at run time |
npm run contracts (contracts/gen.mjs) writes them into each app. In a public clone it writes and checks the web files only, src/web/generated/contracts.ts and src/web/generated/contracts.css. Never edit a generated file.
npm run contracts # regenerate
npm run contracts -- --check # fail when a generated file is out of date
Feature specs
Each file in spec/features/ (account.md, community.md, create.md, feed.md, home.md, play.md, share.md) has one table:
| ID | Behaviour | iOS | Android | Web |
|---|---|---|---|---|
| PLAY-1 | Play runs the version's build for this platform | yes | yes | yes |
yes: built on that app.no: not built yet. The notes under the table say why, or what stands in for it.n/a: does not apply to that app (an App Clip on Android).
npm run parity (spec/parity.mjs) prints, per app, what is built and which behaviours are missing.
Changing something
| The change | Where it starts |
|---|---|
| A colour, a radius, the product's name, a sentence | contracts/, then npm run contracts |
| A behaviour (what a screen or button does) | A row in spec/features/, then each app, or the cell left no on purpose |
| A new server route or field | The server, then each app's API client (src/web/api.ts for the web) |
| How games run on a platform | The engine (templates/crescent-game/src/engine/), never a game or one app's shell |
Contributors outside the project can change the contracts, the specs and the web app. Changes that need the phone apps are built there by the maintainers; mark the iOS and Android cells no and say so in the notes.
The web app
| Route | Screen |
|---|---|
/ | Home: latest played, your games |
/feed | Every player's games |
/create | The conversation with Nova (?game=<id> to change one of yours, ?help=<id> to help build someone else's) |
/play/<buildId> | The player, with the chat about the game beside it |
/settings | Account and credits |
The web app runs in the server's Next.js project, so it shares the API's types and has no deployment of its own. Games are phone-sized in it and run in an <iframe> on a separate play origin (see Builds and versions), talking to the app with postMessage (src/web/play.ts). The client is src/web/api.ts, the conversation store src/web/chat.ts, the player frame src/web/GameFrame.tsx. How the engine itself runs in a browser is in Web.