Deploy the client
The second of the two deployments, and the easy one. The client is a Vite + React app that builds to a folder of static files:
cd clientnpm run build # -> dist/Anything that can serve a folder can host it. There is no server, no runtime, no process to keep alive — which is precisely why nothing here can break the accept-and-poll architecture. The bytes are the same on every host.
Vercel
Section titled “Vercel”The bonus track. About two minutes, no CLI, no credit card — and
client/vercel.json
is already written, so there’s nothing to configure beyond two fields.
-
Push your repo to GitHub.
-
Go to vercel.com/new and import the repository.
-
Root Directory →
clientThis is the field people miss. The Vite project lives in
client/, not at the repo root — point Vercel at the root and the build finds nopackage.jsonand fails. -
Environment Variables → add
VITE_API_URL, set to your backend URL (no trailing slash). -
Deploy.
Vercel detects Vite on its own, but client/vercel.json states it explicitly
anyway:
{ "$schema": "https://openapi.vercel.sh/vercel.json", "framework": "vite", "buildCommand": "npm run build", "outputDirectory": "dist", "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]}The rewrites line is the one worth understanding: it serves index.html for
every path, so deep links and refreshes don’t 404. That’s the standard
single-page-app rule, and it’s why a SPA needs any config on a static host at
all.
Every push to your default branch redeploys, and pull requests get their own preview URL.
GitHub Pages
Section titled “GitHub Pages”Already done for you. The workshop site’s build compiles the client and publishes it alongside the docs and the slides:
https://<user>.github.io/<repo>/ the websitehttps://<user>.github.io/<repo>/slides/ the deckhttps://<user>.github.io/<repo>/demo/ the clientPush to main and .github/workflows/deploy-pages.yml
does the rest. Enable it once at Settings → Pages → Source: GitHub Actions.
The only subtlety is the sub-path. A project page lives under /<repo>/, not at
a domain root, so the client is built with a --base flag —
website/scripts/sync-assets.mjs passes it. That’s also why vite.config.ts
deliberately doesn’t hard-code base:
// Vercel / any root domain npm run build -> base "/"// GitHub Pages sub-path npm run build -- --base=/x/demo/Hard-coding it would break whichever of the two deployments didn’t match.
Then fix CORS, because it will break
Section titled “Then fix CORS, because it will break”Deploying the client creates a new origin, and your backend doesn’t know
about it yet. The symptom is “Failed to fetch” in the browser console, with
a working curl — which is the tell, because CORS is a browser rule, not a
server error.
Set ALLOWED_ORIGINS on the backend to a comma-separated list of every
origin that should be allowed, then redeploy the backend:
ALLOWED_ORIGINS=https://your-client.vercel.app,https://your-user.github.io,http://localhost:5173Three things people get wrong here:
- No trailing slash.
https://x.vercel.app/doesn’t matchhttps://x.vercel.app. - Origin, not URL. Scheme and host only — no
/demo/path on the end, even though that’s where the page lives. - It’s on the backend, and it needs a redeploy. Setting the variable doesn’t restart the service.
Pointing the client at a different backend
Section titled “Pointing the client at a different backend”You don’t have to redeploy to switch backends. The UI has a Backend URL
field, saved in your browser, which overrides VITE_API_URL:
- localhost during the build
- your Render URL after you deploy
- your FastAPI Cloud URL when you want to compare the two
Pasted URLs get their trailing slashes stripped (normaliseBaseUrl in
client/src/api.ts) because //runs 404s on some hosts.
VITE_API_URL is not a secret
Section titled “VITE_API_URL is not a secret”Worth repeating, because it’s the one genuinely dangerous thing on this page:
Vite inlines every
VITE_*variable into the JavaScript bundle at build time. Anyone can read them with view-source.
A backend URL is fine there. A Gemini key or a Supabase key is not — those live
in app/.env and in your backend host’s environment panel, on the server,
forever. The whole reason the backend exists as a separate thing is that the
browser never holds a key.
Other static hosts
Section titled “Other static hosts”Netlify, Cloudflare Pages, Render’s own static sites, GitLab Pages, an S3 bucket — all fine, all free at this scale. Every one of them needs the same three things:
| Build command | npm run build |
| Output directory | dist |
| SPA rewrite | all paths → /index.html |
Plus VITE_API_URL at build time, and the resulting origin added to
ALLOWED_ORIGINS on the backend. That’s the entire contract.
See also
Section titled “See also”- Render — the primary agent host
- FastAPI Cloud — the second agent host
- Other hosts · Vercel — why the backend doesn’t go here
- Naming GitHub deployments — optional; what to do when two providers both write to the Deployments tab
- Troubleshooting — “Failed to fetch” and friends