Deployed

Scratch

A platform for deploying and browsing experimental web apps. Each app is either a container or a set of static files.

HTMLCSSJavaScriptReactTypeScriptFlaskPythonPodman

A Flask application that serves as a public scratchpad for small, self-contained web experiments. Each mini-app lives in its own subdirectory with independent HTML, CSS, and JavaScript, or as a pre-built React bundle dropped in as static output. Runs in a Podman container behind the shared NGINX reverse proxy.

Tech Stack

  • Flask + Gunicorn (Python web server)
  • UV (Python package manager)
  • Podman (containerization)

Each hosted experiment has its own tech stack. The blog post covers each one individually.

Architecture

  • Flask serves each mini-app from its own subdirectory under a named route.
    • /token-bucket serves token-bucket/index.html
    • /settlers-react serves the pre-built React SPA.
  • Vanilla apps are fully self-contained (one index.html, style.css, script.js per experiment). Flask’s send_from_directory delivers them with no special handling.
  • React apps are built with Vite locally and their dist/ output is committed directly into this repo as static files.
  • Flask returns index.html as an SPA fallback for any path that doesn’t map to a real file on disk.
  • The Flask container runs on a dedicated port behind the shared NGINX reverse proxy.
    • Traffic to scratch.alexandershank.com routes to this container.
    • Other apps on the same VPS are isolated from this traffic.
  • UV handles Python dependency resolution inside the container, keeping the image build fast and reproducible.

Challenges

  • React apps served from a subpath require something like base: '/settlers-react' in vite.config.ts so that Vite generates asset URLs like /settlers-react/assets/index.js instead of /assets/index.js.
    • Without this, the browser requests assets from the root and the SPA fails to load.
  • Flask does not automatically handle SPA client-side routing. A catch-all route checks whether the requested path maps to a real file on disk and falls back to index.html if not.
    • Direct navigation and page refreshes return a 404.

Learnings

  • Flask works well as a minimal static file server. For a scratchpad with no server-side logic, routing paths through Flask adds little overhead.
  • Committing the React dist/ output into the repo decouples the settlers-react build from the scratch-app deployment. The scratch app can be redeployed without rebuilding the React app, and vice versa.
  • Structuring each experiment as a self-contained subdirectory keeps them isolated. A broken experiment does not affect others, and adding a new one only requires a new directory and a route.

For the Future

  • Automate the React build-and-copy step so settlers-react/dist/ is updated automatically rather than committed manually after each change.
  • Stateful experiments.
  • Leverage sever-side capabilities, instead of static web pages.