Deploy one app to all six providers
Start state: any scaffolded app that runs locally. End state: the same app live on Cloudflare Pages, Vercel, Netlify, Surge, Heroku, and GitHub Pages — and a clear picture of what pnpm deploy actually does per host. The deploy DX contract: login flows launch for you, SPA fallbacks are written for you, and the provider choice is remembered per app.
1. See where you stand
pnpm deploy --providers
Expected: a status table —
provider status
cloudflare logged in Cloudflare Pages
vercel login needed Vercel
netlify login needed Netlify
surge login needed Surge
heroku CLI missing Heroku (static buildpack)
github-pages logged in GitHub Pages
"login needed" is fine — each provider's official login flow launches interactively the first time you deploy to it. "CLI missing" means installing that provider's CLI first (heroku and gh are the two that need a real binary; the rest run via npx).
2. Rehearse with --dry-run
pnpm deploy launchpad --dry-run
Expected: the plan and the full SPA build run, but no network deploys happen — the last line reads "dry run complete — would deploy apps/launchpad/dist to Cloudflare Pages as geenius-mvp-launchpad". Use this to catch build breakage before touching a host.
3. Deploy to each host
The command shape never changes — only --to does:
pnpm deploy launchpad --to cloudflare
pnpm deploy launchpad --to vercel
pnpm deploy launchpad --to netlify
pnpm deploy launchpad --to surge
pnpm deploy launchpad --to heroku
pnpm deploy launchpad --to github-pages
For a Convex app, every run first pushes the backend (reusing the project recorded in .env.local — never a duplicate), builds the SPA against prod URLs, then hands the dist/ directory to the provider adapter. What each adapter does, including the SPA fallback it writes so client-side routes survive a hard refresh:
- cloudflare — creates the Pages project if missing, wrangler pages deploy; no fallback file needed (the template ships public/_redirects) → https://<name>.pages.dev
- vercel — writes vercel.json with a rewrite of /(.*) to /index.html, then vercel deploy --prod → *.vercel.app
- netlify — writes _redirects ("/* /index.html 200"), sites:create then deploy --prod → *.netlify.app
- surge — copies index.html to 200.html (Surge's SPA convention), surge <dir> <name>.surge.sh
- heroku — stages dist/ with static.json (routes /** → index.html) + the heroku-community/static buildpack, tars it, and drives the Heroku Build API; polls until the build succeeds → *.herokuapp.com
- github-pages — copies index.html to 404.html (the Pages SPA trick) and writes .nojekyll, force-pushes a gh-pages branch, enables Pages via the API → https://<owner>.github.io/<name>/
After any Convex deploy, SITE_URL on the prod deployment is synced to the real frontend URL — that is what makes sign-in work on the deployed origin. Deploying to a new provider re-syncs it, so the LAST host you deployed to is the one where auth works. One production backend, one canonical frontend origin at a time.
4. The memory file
cat apps/launchpad/.deploy.json
# { "provider": "github-pages", "url": "https://you.github.io/geenius-mvp-launchpad/" }
The last provider and URL are remembered per app — from now on a bare pnpm deploy launchpad goes back to the same host. Every run also appends to tmp-deploy-summary.txt at the repo root, one line per app: slug | url | backend.
What you built
- The same app live on six hosts, each with the correct SPA fallback written automatically
- A rehearsal habit: --dry-run to validate the build before any network call
- Per-app deploy memory: --to once, then just pnpm deploy <slug> forever