Skip to content

Deployment (Cloudflare Pages)

Every app is a static build. A GitHub Actions workflow builds the monorepo and deploys each app to its own Cloudflare Pages project with wrangler. The workflow runs on every push to main and on every pull request, so each PR gets its own preview URL.

All web surfaces live under esimm.app, one subdomain per project.

SubdomainCloudflare projectWhat it serves
newsstand.esimm.appmars-newsstandCommunity Newsstand app
miam.esimm.appmars-miamMars in a Moment app
docs.esimm.appmars-docsThis documentation site
design.esimm.appmars-designDesign system (Ladle)
innovation.esimm.appmars-innovationInnovation app (not built yet)
install.esimm.appmars-installInstaller download page (not built yet)

The last two projects are added when those apps exist.

The workflow is .github/workflows/deploy.yml.

  1. Typecheck runs once for the whole monorepo. If it fails, nothing deploys.
  2. Deploy runs as a matrix, one leg per app. Each leg installs dependencies, builds just that app, and deploys its dist folder to the matching Cloudflare Pages project.
  3. On a pull request, each leg posts the preview URL as a comment on the PR.

To add a new app later, add one entry to the matrix include list in the workflow. Nothing else changes.

Register esimm.app in the Cloudflare dashboard (Registrar) and let Cloudflare manage its DNS.

In the Cloudflare dashboard, go to My Profile then API Tokens then Create Token. Use the Edit Cloudflare Workers template, or a custom token with the Cloudflare Pages: Edit permission. Copy the token.

Also copy the Account ID from the dashboard sidebar.

In the GitHub repo, go to Settings then Secrets and variables then Actions, and add:

  • CLOUDFLARE_API_TOKEN — the token from step 2
  • CLOUDFLARE_ACCOUNT_ID — the account ID from step 2

Each app needs a Pages project of type Direct Upload (not Git connected, because the workflow uploads the build). Create one per app.

From the dashboard: Workers & Pages then Create application then Pages then Upload assets. Name the project (for example mars-miam) and set its production branch to main.

Or from the command line:

Terminal window
npx wrangler pages project create mars-newsstand --production-branch=main
npx wrangler pages project create mars-miam --production-branch=main
npx wrangler pages project create mars-docs --production-branch=main
npx wrangler pages project create mars-design --production-branch=main

In each project, open the Custom domains tab and add the subdomain from the table above (for example miam.esimm.app). Cloudflare creates the DNS record automatically because it manages the esimm.app zone.

  • Production (push to main): https://<subdomain>.esimm.app
  • Default Pages URL: https://<project>.pages.dev
  • PR previews: https://<branch>.<project>.pages.dev, also posted as a PR comment
  • miam is a single-page app with client-side routing, so it ships public/_redirects with /* /index.html 200. This is required or deep links like /world return 404.
  • newsstand is multi-page (index.html, controller.html, screen.html, simulator.html), so each page is served directly and no redirects file is needed.
  • docs and design are static multi-page builds and need no redirects.
  • Node comes from .nvmrc at the repo root (currently 22). The workflow reads it; Cloudflare is not building, so only the workflow matters.
  • pnpm comes from the packageManager field in the root package.json.
  1. Create the app folder in apps/<name> with a build script that outputs to dist.
  2. Create a Cloudflare Pages project named mars-<name> (Direct Upload, production branch main).
  3. Add an entry to the matrix in .github/workflows/deploy.yml:
    - app: <name>
    project: mars-<name>
  4. Add the custom domain <name>.esimm.app in the project settings.