# CatGO — Model C: hosted static frontend + user-owned backend Model C ships **one hosted static SPA** (e.g. `https://app.catgo-ucsd.org`) that every user points at **their own** CatGO backend — running on their laptop (`localhost`) or reached from a phone via Tailscale. You (the maintainer) host only static files. Each user keeps their own SSH credentials, cluster access, and data on their own machine. There is **no server-side component and no shared backend** to operate. --- ## Three deployment models (context) | Model | What's hosted | Backend location | Liability for maintainer | Notes | |-------|---------------|------------------|--------------------------|-------| | **A — Termux** | Nothing (user installs locally) | On the user's Android device (Termux) | None | Fully self-contained on phone; heaviest user setup. | | **B — hosted backend** | Frontend **and** backend (you run the server) | Your server | **High** — you hold/run user code, SSH creds, compute | Easiest for users, but you operate a real backend with their credentials. | | **C — hosted frontend + user backend** ⭐ | Static SPA only | The user's own laptop / Tailscale device | **Low** — you host static files only | **Recommended default.** Users keep their own SSH creds and data; you never touch them. | ⭐ **Model C is the recommended low-liability default.** You serve only an immutable static bundle; all compute, credentials, and data stay on the user's machine. --- ## Maintainer: build & host the static SPA Build a **connect** bundle (NOT `deploy:build` — that one injects a 503 fetch interceptor via `VITE_STATIC_ONLY` and blocks all backend calls, which is wrong for Model C): ```bash pnpm build:connect ``` This runs `vite build --config vite.desktop.config.ts` with **no** `VITE_STATIC_ONLY`, so there is no 503 interceptor and the SPA can reach a real user backend. Output goes to `build-desktop/` at the repo root. It is a pure static SPA — no server-side runtime. Host `build-desktop/` on any static host: ### Cloudflare Pages (works out of the box) `vite.desktop.config.ts` auto-emits a Cloudflare `_redirects` file (`/* /index.html 200`) at `closeBundle`, so SPA routing works with no extra config. Set the build output / publish directory to `build-desktop/`. ### Vercel (uses `vercel.json`) Deploy `build-desktop/` and use the `vercel.json` in this directory — it rewrites all paths to `/index.html` for SPA routing: ```json { "rewrites": [ { "source": "/(.*)", "destination": "/index.html" } ] } ``` ### GitHub Pages (manual SPA fallback) GitHub Pages has no rewrite engine. After building: 1. Copy `index.html` to `404.html` inside the published bundle so deep links fall back to the SPA. **(Do this in your deploy step; it is intentionally not generated by the build.)** 2. Add a `CNAME` file containing your custom domain (e.g. `app.catgo-ucsd.org`) if you use one. --- ## User: same device (laptop) Run your own backend, allowing the hosted origin via CORS: ```bash CATGO_ALLOWED_ORIGINS=https://app.catgo-ucsd.org python server/main.py # or the light backend: CATGO_ALLOWED_ORIGINS=https://app.catgo-ucsd.org CATGO_THIN=1 python server/main.py ``` The backend binds `0.0.0.0:8000`. Then: 1. Open `https://app.catgo-ucsd.org` in your browser. 2. In the app's **Backend URL** setting, enter `http://localhost:8000`. This works even though the page is HTTPS and the backend is plain HTTP: browsers grant a **loopback secure-context exception**, so an HTTPS page is allowed to call `http://localhost` (and `http://127.0.0.1`) without mixed-content blocking. WebSocket (`ws://localhost`) is allowed under the same exception. > ⚠️ **Safari / iOS weak spot:** Safari is the least reliable here. Its handling > of the HTTPS-page → `http://localhost` exception is stricter/flakier than > Chrome and Firefox, and mixed-content / private-network behavior can block the > call. If localhost fails on Safari/iOS, use the Tailscale approach below > (both ends HTTPS), which sidesteps the exception entirely. --- ## User: phone → computer (cross-device, via Tailscale) The loopback exception is **local only** — it does **not** apply across devices. So an HTTPS page on your phone **cannot** call `http://:8000` on your computer (mixed content / insecure origin). The fix is to give the backend a real HTTPS origin with a valid certificate using Tailscale: 1. On the **computer** running the backend, expose port 8000 over your tailnet: ```bash tailscale serve --bg 8000 ``` Tailscale now serves the backend at `https://..ts.net` with a valid TLS cert. 2. Start the backend allowing the hosted origin: ```bash CATGO_ALLOWED_ORIGINS=https://app.catgo-ucsd.org python server/main.py ``` (The CORS regex already allows `https://..ts.net`, so the backend accepts the proxied requests; you still set `CATGO_ALLOWED_ORIGINS` for the hosted SPA origin.) 3. On the **phone** (joined to the same tailnet), open `https://app.catgo-ucsd.org` and enter `https://..ts.net` in the **Backend URL** setting. Now **both ends are HTTPS** → no mixed-content blocking, and secure WebSocket (`wss://`) works for the terminal and live updates. --- ## Security note Each user's backend exposes **both** the SPA **and** the full API, and CatGO ships **no authentication layer**. Anyone who can reach the backend can run terminal commands, submit HPC jobs, and read files on that machine. - Keep the backend on **loopback** (`localhost`) or restricted to your **Tailscale** tailnet (ACL-limited devices you control). - **Never** expose the backend on the public internet or a public LAN IP. - `tailscale serve` keeps the endpoint inside your tailnet; do **not** use `tailscale funnel` (public exposure) for CatGO.