Deployed
Scratch
A platform for deploying and browsing experimental web apps. Each web app is a collection of static files and an optional backing-API implemented in Python.
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)
- Postgres (persistent storage for stateful experiments)
- psycopg (Python Postgres driver)
- Pydantic (typed domain models and request validation)
- Ruff, mypy (strict), and pytest for the shared Python code
- React and Vite for the richer experiments, with Mantine and TypeScript in some of them
Each hosted experiment ships its own frontend stack, but all of them sit on top of one shared Python backend with a single set of dependencies, linting, and type checks. The blog post covers each experiment individually.
Architecture
- Frontends and the Python backend are kept separate. Each frontend is a standalone app with its own Node and Vite tooling. All of the Python, including every API, lives in one shared environment with a single set of dependencies, linting, and type checks.
- Flask serves each mini-app from its own subdirectory under a named route.
/token-bucketservestoken-bucket/index.html/settlers-reactserves the pre-built React SPA./guestbookserves a React SPA backed by a Flask REST API and Postgres./joustserves the React tournament app backed by its own Flask REST API and Postgres./gazetteserves The Liminal Gazette, an index of standalone HTML pages styled like the real estate classifieds section of a newspaper. Each page is a self-contained file with its own “address.” A sort control displays files by “newest listed” or alphabetical order.- And more!
- Vanilla apps are fully self-contained (one
index.html,style.css,script.jsper experiment). Flask’ssend_from_directorydelivers them with no special handling. - The settlers-react app is built with Vite in a separate repo, and its
dist/output is dropped into this repo’s working tree as static files (gitignored, not committed). The guestbook and joust apps are compiled during the Docker build instead. - Flask returns
index.htmlas an SPA fallback for any path that doesn’t map to a real file on disk. - Stateful apps follow the same layered structure. A thin Flask blueprint handles serialization, a service layer holds the logic, a repository owns the SQL, and Pydantic models carry the data. They all share one Postgres client with connection pooling.
- Stateful experiments use a shared Postgres instance on the VPS. Each app gets its own database, its own limited role, and its own connection pool. The
*_DATABASE_URLenvironment variables are injected at runtime via docker-compose.- NGINX caps requests per IP upstream before they reach the Flask container.
- The Flask container runs on a dedicated port behind the shared NGINX reverse proxy.
- Traffic to
scratch.alexandershank.comroutes to this container. - Other apps on the same VPS are isolated from this traffic.
- Traffic to
- The production image builds in stages. Node stages compile the guestbook and joust React apps (settlers-react is copied in pre-built), then the final stage copies the output into a small Python runtime image.
- UV handles Python dependency resolution, which keeps the build fast and reproducible.
Challenges
- React apps served from a subpath require something like
base: '/settlers-react'invite.config.tsso that Vite generates asset URLs like/settlers-react/assets/index.jsinstead 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.htmlif not.- Direct navigation and page refreshes return a 404.
- Now that there are stateful applications on the VPS, flyway migrations and more complex deployments are needed to avoid data loss or corruption.
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.
- Dropping the settlers-react
dist/output into the repo working tree 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.
- Separating the frontends from one shared Python backend pays off as the collection grows. A single set of dependencies, linting rules, and type checks covers every API, and the shared layered structure means each new stateful app slots in the same way.
For the Future
- Automate the React build-and-copy step so
settlers-react/dist/is updated automatically rather than copied in manually after each change.