The game in two sentences
Every card is a real piece of music, with its artwork, a 30-second preview and a rarity from Common to Legendary. You dig through crates like at a record shop, complete albums, trade, and face your friends in blind tests. A catalog of about 18,000 tracks, refreshed automatically every week, in French and English.
My personal goal: helping people discover music and other cultures. Every week the game highlights three albums, two classics and one discovery, each with its story.
First lines of code on 25 September 2026, version 1.0 live on setlist.gg on 11 October, 16 days later. Since then, one production release a week, on Wednesday at 2 am.
My role: setting the direction
I held every role of a small studio: game designer, architect, ops and support.
I wrote the code with an AI assistant (Claude Code), and I say so openly. What the AI does not do for me is decide: what the game should be, what is fair for players, what ships to production and when. Every feature starts from an intent I formulate, and every release goes through my approval.
I also set up a systematic review: a second agent reads every batch of changes before release, for security, performance and regressions. On the latest version, four review passes produced fourteen fixes, all merged before shipping. Four real vulnerabilities were stopped before production, including account creation without a confirmed email through OAuth and a load amplification through the "typing" signal. None of them reached players.
What I learned about game design
I had never made a game. The hard questions were not technical:
- The economy. Players earn "notes" and spend them on digs. An over-generous reward devalues everything else. Before any new bonus, I look at real prices and the median player wallet.
- Rarity. It does not come from an arbitrary roll: it follows each track's lasting popularity (Deezer, Last.fm, Wikipedia, longevity). A Legendary is a song everyone has known for decades.
- Frustration. A pity counter guarantees an Ultra Rare after 30 digs without one. Enough to avoid frustration, not enough to make rarity meaningless.
- Dopamine, measured. A Legendary gets its full-screen celebration. An Ultra Rare gets a quieter effect. And players who dislike animations can turn them off in the settings.
- No real money. The premium mode, Backstage, is paid only with in-game notes. That is a choice: the music API I use is not commercial, and I do not want a pay-to-win game.
- Abuse resistance. Referrals, trades, rewards: every mechanic that gives something out was designed not to be farmable (limited accounts per network, rewards paid once, sensitive actions restricted to confirmed email addresses).
The architecture
Deliberately simple, because one person has to be able to understand and repair all of it.
Monitoring and alerts: Prometheus, Alertmanager, Discord.
The game
- Backend: PocketBase (Go and SQLite), with all game logic in JavaScript hooks: 66 files, about 11,000 lines.
- Frontend: SvelteKit and Svelte 5, an installable web app on iPhone, about 24,000 lines, two languages and more than 1,400 translated strings.
- Real time: messages, read receipts, a "typing" indicator, a friends activity feed.
- 45 versioned, replayable schema migrations: no database change is ever made by hand.
Production
- One OVH VPS (2 vCPU, 4 GB of RAM, €8 a month), on Docker Swarm: 14 containers in four separate stacks (Traefik and Let's Encrypt edge, monitoring, game, websites).
- The game uses 0.05 CPU cores on average, with a measured peak at 0.9.
- Prometheus, Grafana, Loki, Alertmanager, 12 alert rules, each with its likely cause and diagnostic command, and a monitoring tab built into the game's admin panel.
- Database replicated every second (Litestream) to my homelab over a WireGuard tunnel: 7 days of restore points, one full snapshot a day.
Staging
- My homelab hosts a full copy of the game, built from the development branch. Everything is tested there before it reaches production.
- The real game rules, but no public accounts: I never test in production.
A custom game API
All of Setlist's logic goes through an API I designed on top of PocketBase: 119 routes, 97 of them restricted to signed-in players. It is what enforces the game rules, server-side, without ever trusting the client: card draws and the pity counter, blind tests and the ladder, trades and the auction house, groups, real-time messages, rewards and abuse prevention.
It relies on six external sources (Deezer for the catalog and previews, MusicBrainz, Last.fm, Wikidata and Genius for release years, popularity and credits, Discord for the community), with queues and quotas to respect their limits.
It also serves public pages for search engines (card and artist pages, sitemaps), and exposes its own metrics to Prometheus on an endpoint restricted to the private network or protected by token. Per-IP rate limiting protects the whole thing. In numbers: 68 ms median latency on game actions, 271 ms at the 95th percentile, measured by Traefik over the first 49 hours.
Choices and their trade-offs
- SQLite rather than PostgreSQL
- One machine, one file, simple backups, very fast queries at this scale. The trade-off: no horizontal scaling. If the game ever grew seriously, that would be the first project.
- Docker Swarm rather than Kubernetes
- Swarm does what I need (rolling deployments, automatic rollback, secrets, networks) without the operational weight of Kubernetes. For a service on a single machine, Kubernetes would have been cost without benefit.
- A weekly release train
- At first I deployed several times a day. I stopped: everything validated waits on a branch, and a script ships it all on Wednesday at 2 am. If there is nothing new, there is no release. The script builds a clean copy of the code, deploys, checks the game's health, and rolls back to the previous version on its own if that check fails. Players get regular updates without living with constant instability.
- Few alerts, but real ones
- An alert that fires for nothing ends up ignored. After a series of false positives, I rewrote every rule: an outage is only declared after 5 minutes with no response at all, slowness is only measured on successful requests, and reminders are spaced out. I also added capacity alerts: they tell me, with numbers, when it is time for a bigger server, rather than finding out during a player peak.
A real incident
On 13 October, my homelab shut down during the night. The game itself saw nothing: it runs on the VPS. But the database replication towards the homelab got stuck on a dead connection, and it did not recover when the server came back.
My alert checked that the replication service was running, not that it was actually copying: nothing fired. I found out the same day, while preparing an economy analysis from a copy of the database: the data was a day old.
In numbers: zero data lost, about 25 minutes from discovery to a verified restore. After the repair, I added an alert on actual replication lag, and made the silent copy script loud about failure. The same blockage would now be detected within 30 minutes, against about fifteen hours that day. The lesson I keep: watching that a process runs does not tell you it is doing its job.
What I would do differently
- A second backup destination, independent from my homelab, from day one.
- An automatic, daily restore test, rather than discovering a backup is unusable on the day you need it.
- The weekly release train from week one, instead of adopting it after a few deployments too many.
What Setlist says about how I work
I design systems a small team can actually operate. I prefer a simple, well-monitored architecture to an impressive one nobody understands at 3 am. And I discovered I enjoy designing a player's experience as much as the infrastructure that runs it.