Infrastructure9 min read
The boring stack that runs EloCoach, and why each piece is there
EloCoach is a one-person project with real users, which means every piece of infrastructure has to justify itself against a simple test: does it make the product cheaper, more reliable, or simpler to run, and if not, why is it here? This is the whole stack and the reason for each part. At the bottom there is an honest list of what I would change.
The first decision: do the heavy work on the phone
Stockfish and Maia run on your device. That is the single biggest cost decision in the project, and it is boring to look at, because nothing on the diagram represents it. Every position in your game gets analysed by an engine you downloaded once. We do not pay per game, we do not queue, and a review does not slow down when more people join. The only paid, per-use thing in the loop is the language model that explains the result.
Koyeb: one place for the site, the API and the database
The marketing site, the backend and Postgres all live on Koyeb. I wanted one dashboard, one bill and one deploy model, and the price for a small always-on service is hard to beat. There is no cluster to babysit. The tradeoff is real: the backend runs on a small instance, and I have had to be strict about memory to keep it from running out on startup.
Postgres, and PGlite on my laptop
Production uses Koyeb’s managed Postgres through Drizzle ORM. Locally the same schema and migrations run on PGlite, an in-process Postgres, so a fresh checkout needs no database server. Migrations run at boot. When you are the only engineer, “it runs the same on my machine” is a feature worth paying for in design.
Inngest: so nothing important depends on a request staying open
Game syncing, fact extraction after each coach reply, token refresh, the daily memory reconciliation and the weekly digest all run as Inngest functions. They retry on failure and report what happened, and I did not have to run a queue or a worker. The coach’s answer streams to you and closes. The memory update happens afterwards, durably, whether or not you are still looking at the screen.
OpenRouter: model choice is a row in a table
Every language model call goes through OpenRouter. Which model serves which job is data, not code, so changing one is a database row and not a deploy. Each user also gets their own key, which gives us a per-user spend limit at the source. I will write about both in the cost and model posts.
Resend and PostHog: knowing what happened
Resend sends the welcome email and the weekly digest from a dedicated subdomain so transactional mail never touches the main domain’s reputation. PostHog covers product analytics, error tracking and session replay. The app and the backend report the same user ID, so one person’s bad afternoon can be followed from the tap, through the API, to the error. A lot of the bugs I have fixed this summer showed up there first.
Expo and Adapty: one codebase, no receipt code
The app is Expo and React Native, so Android and iOS share a codebase. Adapty handles the paywall and subscription state across both stores, which saves me from writing receipt validation twice. One honest gap: the backend does not yet read subscription state from Adapty, which is the next piece of work before paid tiers go live.
Cloudflare, GitHub and Giphy: small parts, real jobs
- Cloudflare holds DNS and sits in front of the site and API. A side effect worth knowing: behind two layers of proxy, the IP address a server sees is the proxy’s, so per-visitor logic has to read the forwarded client address instead.
- GitHub is the source and the deploy button. Koyeb watches one branch for the site and one for the backend. Shipping is a fast-forward merge, which means every release is a commit you can point at and roll back to.
- Giphy supplies the mood GIF on the screen you see before a review starts. It is optional. Without it, the slot stays a plain surface, and nothing else notices.
How this adds up
- Low cost: the expensive compute is on your phone, and everything else is a small managed service with a free or cheap tier.
- Reliability: background work is durable and retried, and a failed model call falls back to another model instead of failing the review.
- Simplicity: few moving parts, each doing one job I do not want to build myself.
- Scaling: the pieces that grow with users are the database and the language model bill. Everything else scales for free because it is on the user’s device.