Getting It Off Localhost
Railway, Vercel, and the small mistakes that made deployment take longer than architecture
Before deployment: a phone on the same WiFi
Before any cloud infrastructure, the first real test was simpler: serve the Angular PWA over the local network and open it on a phone. No App Store, no certificates, no deployment pipeline.
ng build --configuration production npx http-server dist/squint-frontend/browser -p 8080 -a 0.0.0.0 --cors
On Android, navigate to the laptop’s IP address on port 8080, tap the three-dot menu, add to home screen. A PWA installed on a phone in under five minutes. This surfaces layout and touch-target problems that a desktop browser never shows — and costs nothing to set up.
It also surfaced three things immediately: the Angular router needed a SPA fallback or every direct URL returned a blank screen, the backend needed --host 0.0.0.0 or the phone could not reach it, and the environment file was still hardcoded to localhost rather than the laptop’s network IP. Each one a five-minute fix. Each one invisible until a real device was in the room.
The local WiFi test is the right first step before any deployment. It answers the question that localhost cannot: does this work on the device it was built for?
The venv went to GitHub. All of it.
The .gitignore said .venv/. The actual folder was named venv/. One character. The initial git add . staged the entire Python virtual environment — thousands of files, hundreds of megabytes.
The push failed partway through. The fix required deleting the git history entirely and starting clean:
rmdir /s /q .git git init git add . git commit -m "initial commit" git push --force origin main
Same pattern as the encoding errors in Part 3: a one-character mismatch with systemic consequences. The gitignore for a Python project with a venv should explicitly name both venv/ and .venv/. It is a two-second addition that was not made until after the hour it cost.
Railway’s free tier was paused the week it was needed
The plan was Railway for the FastAPI backend. Free tier, deploys from GitHub, auto-detects Python. First attempt:
Railway had restricted the free tier that week. No warning, no timeline. Render worked as a fallback, but sleeping after inactivity was a poor fit for an API that needed to answer within a few seconds. The durable fix was to stop treating free infrastructure as a plan and pay for the backend that matched the workload.
Infrastructure availability is an external dependency. It does not care about your launch timeline.
nixpacks was deprecated. Then $PORT did not work.
Railway’s first build attempt used nixpacks as the builder. Deprecated. Updated to railpack. Build succeeded. Then:
Railway assigns a dynamic $PORT to each deployment. The railway.toml start command used $PORT correctly. Uvicorn was ignoring it and defaulting to 8000. Railway was routing traffic to a different port. The fix: add PORT=8080 as an explicit environment variable in the Railway dashboard, which Railway then routes correctly.
The deeper issue: $PORT in a shell command inside a TOML file does not always resolve the way it would in a shell. When a deployment is failing silently, check what port the process is actually listening on before assuming the config is wrong.
Vercel worked first time. Angular needed one config file.
The Angular PWA builds to static files. Static files belong on a CDN. Vercel connects to a GitHub repo, detects the framework, and deploys on push. The one thing it needed: a vercel.json to run the API URL injection script before the build, so the production frontend pointed at the Railway URL rather than localhost.
{
"buildCommand": "node scripts/set-api-url.js && ng build --configuration production",
"outputDirectory": "dist/squint-frontend/browser",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
The rewrites rule is the SPA fallback from the WiFi test — same problem, same fix, now baked into the config rather than a CLI flag. The Angular router needs every route to serve index.html or direct URLs return 404.
Frontend was live in one push. The only cost was the config file that should have existed from the start.
{"status":"ok","message":"Squint API running"}
After the venv commit, the paused free tier, the deprecated builder, the PORT variable, the bad gateway, the Railway dashboard variable, and one more redeploy — the backend URL returned:
It was not a particularly dramatic moment. It was eleven o’clock at night. The next step was updating the Angular environment file to point at the Railway URL, pushing to GitHub, waiting for Vercel to build, and opening the URL on the phone.
It worked on the phone. That was the moment the project stopped being a local experiment.
The decisions that matter are made before the first push
The infrastructure choices were right. A Claude Vision API call takes two to four seconds. Serverless functions with cold starts and short timeouts would have failed unpredictably. A persistent backend and a static frontend were the right shape for the workload.
The hidden cost was all the setup work that should have existed before the first git add: a gitignore that matched the actual folder, a vercel.json for SPA routing, a port configuration that matched the host, and the assumption that a free tier might disappear when needed.
None of these are hard. All of them are the kind of thing you learn on the second project and pay for on the first.
What four posts actually described
These four posts describe a feedback problem that became a product, an agent team designed instead of improvised, the mess of building with AI assistants, and the work of making the thing reachable by real users.
The result is concrete: a beginner can draw a sphere, photograph it, upload it, and get feedback that tells them what is wrong and what to try next.
The app shipped because the sessions turned decisions into files, fixes, and deploys. The agents helped by making those sessions less confused about what to build. That is the real scope of what they contributed.
Stack: Angular 21 PWA — FastAPI (Python) — Ollama / Gemini 2.5 Flash / Claude Sonnet (evaluation, still running) — Vercel (frontend) — Railway (backend). Repository currently private while the project is still evolving.