Skip to main content

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:

  1. Contracts (contracts/): names, colours, metrics and every shared sentence, generated into each app.
  2. One server API: the same routes, sessions and shapes for every app (see Architecture). X-Crescent-Client: ios | android | web picks each version's export (see Builds and versions).
  3. 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​

FileHolds
contracts/identity.jsonThe product's names, the share host and URL scheme
contracts/tokens.jsonThe look: colours, corner radii, the gutter, layout limits (the web app's one column is capped at layout.maxWidth, 1024)
contracts/copy.jsonEvery 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:

IDBehaviouriOSAndroidWeb
PLAY-1Play runs the version's build for this platformyesyesyes
  • 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 changeWhere it starts
A colour, a radius, the product's name, a sentencecontracts/, 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 fieldThe server, then each app's API client (src/web/api.ts for the web)
How games run on a platformThe 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​

RouteScreen
/Home: latest played, your games
/feedEvery player's games
/createThe 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
/settingsAccount 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.