
What building a production app with Claude Code actually looks like — the good, the tedious, and the things it still gets wrong
This spring, I wanted an answer to a stupidly simple question: how did my town vote?
The data exists. It’s public. The French Ministry of the Interior publishes every municipal election result down to the last village. But try actually looking at it. You get administrative spreadsheets, scattered CSVs, and websites that were clearly never opened on a phone.
So I built the thing I wanted. Alone, evenings and weekends.
Two and a half months later, VoxMap is live on the App Store and Google Play: an interactive map of France where every one of the ~35,000 communes is colored by how it voted, across four election cycles (2008, 2014, 2020, 2026), backed by a data pipeline pulling from a dozen public sources.
Roughly 42,000 lines of code. A .NET MAUI mobile app, an ASP.NET Core backend, PostgreSQL/PostGIS, Redis, Docker, Nginx, a sovereign French host. No team. No agency. No inherited codebase.
I wrote all of it with Claude Code.
This article is not “AI wrote my app and it was magic.” It’s what the process actually felt like, where it genuinely changed what one person can ship, and — more usefully — where it fell flat on its face.
What I actually built
A quick sense of scope, because “I built an app” hides a lot:
Mobile (.NET MAUI, iOS + Android). Interactive map, commune search with autocomplete, detail sheets, year comparison, push notifications, in-app purchases, an ad layer, device management, account flows.
Backend (ASP.NET Core, C#). JWT auth with refresh-token rotation, a tile server, REST endpoints, an admin panel, RGPD deletion workflows, a RevenueCat webhook, cron jobs.
Data. PostgreSQL 16 with PostGIS holding commune geometries and about 1.3 million election result rows, plus civic datasets (income, healthcare, schools, transport, mobile coverage, air quality) normalized from INSEE, ANFR, ARCEP, and others.
Infrastructure. Docker Compose on a French VPS, Nginx with Let’s Encrypt, Redis caching, self-hosted analytics, automated database migrations on deploy.
The piece I’m most proud of is invisible: the map doesn’t ship 35,000 polygons to the phone. The backend renders raster tiles server-side — 256×256 PNGs over the standard XYZ scheme — with geometry simplification that adapts to zoom level, cached in Redis. The phone just displays images. That’s what makes it fast on a mid-range Android in a train tunnel.
How the work actually went
The mental model that clicked for me: I stopped being a typist and became a reviewer.
Not a manager — a reviewer. That distinction matters. Managers delegate and check the outcome. Reviewers read every line.
A typical session looked like this:
“The map needs to color each commune by the winning party’s nuance code. I want the colors to come from the Ministry’s official codes, not my own choices. Here’s the schema.”
Claude would come back with a full implementation: the SQL with a LATERAL JOIN, the color mapping, the tile rendering. Usually 80% right. Then the real work started — the part where I earn my title:
- Why are you querying the geometry table twice?
- This breaks for Paris/Lyon/Marseille, they’re districts, not communes.
- That index won’t be used with this WHERE clause.
The 80% it gets right is the boring 80%. Boilerplate, wiring, the fifth CRUD endpoint, the migration file, the Razor page that looks like the other Razor pages. That work is not where craft lives, and handing it off is pure gain.
The remaining 20% — schema design, the caching strategy, deciding that PLM districts need their own data model — is where I spent my thinking. Claude was genuinely useful there too, but as a sparring partner, not an oracle. I’d propose an approach, it would poke holes, I’d revise.
Where it changed what’s possible
Breadth, not depth. I’m a backend engineer by trade. Left alone, I’d have written a competent API and a mediocre app. With Claude, I shipped native iOS and Android, wrote CSS I’m not embarrassed by, and configured Nginx without three days of Stack Overflow. It didn’t make me a better backend engineer. It made me dangerous outside my lane — which, for a solo project, is the whole ballgame.
Momentum survives context switching. The killer of side projects isn’t difficulty, it’s the twenty minutes of re-orientation every time you sit down. “Where was I? What does this class do?” Being able to ask “remind me how the tile cache invalidation works” and get a straight answer collapses that to zero. I’d guess this alone doubled my effective hours.
The tedium tax drops to near zero. French and English localization files, six subscription tiers, twelve civic-data importers with slightly different CSV formats. Multiply that by ten weeks of evenings and it’s the difference between shipping and quietly abandoning it in November.
Where it fell flat
This is the part most “I built X with AI” posts skip, so let’s be specific. Every example below is real.
It cannot tell you what’s actually broken at runtime.
My newsletter signup form looked perfect. The code was clean, it compiled, it deployed. It also returned HTTP 400 on every single submission. The form used a hardcoded action="/newsletter" attribute, which meant Razor’s tag helper never activated, which meant no antiforgery token was generated, which meant the server rejected every POST.
Nothing in the code looks wrong. You find this by hitting your own endpoint with curl and seeing a 400 come back. I caught it because I test in production before I trust anything. If I’d shipped on “it compiles and looks right,” I’d have silently collected zero emails while congratulating myself.
It defaults to plausible, not correct.
I asked for a promotional PDF. It came back with background-clip: text for a gradient headline — beautiful on screen, and completely unsupported by Chrome’s print engine. The exported PDF had blue rectangles where the numbers should be. Reasonable code, wrong context.
Same category: styling a section on a light background when the entire site uses a dark theme. The title inherited white-on-white and vanished. The CSS was valid. It just hadn’t looked at the room it was walking into.
It has no eyes.
Related to the above, and worth stating plainly: it can’t see rendered output. Every visual bug in this project — the invisible heading, the print artifacts, the store badges that failed to load because of a relative path — was found by me looking at a screenshot. If your workflow doesn’t include actually looking at the thing, the model has no way to catch these.
It doesn’t know your environment drifted.
My CI runner died with JAVA_HOME is set to an invalid directory. Cause: Homebrew had bumped openjdk@21 from 21.0.11 to 21.0.12, and the runner had the versioned path hardcoded. No amount of reading application code finds that. You find it by running ls on the path that’s supposedly broken.
And it won’t check your business state.
The most instructive one. I recorded a demo video using our App Store review account, assuming it was premium. The video came out with test ads plastered across the bottom and none of the premium data visible. The code was fine. The account had expired three weeks earlier. One SQL query would have told me. I only found it because a human — the app’s owner — said “you should have used the other account.”
What I’d tell you if you’re about to try this
Test in production, early and suspiciously. Compiling is not working. Deployed is not working. Observed is working. Every serious bug in this project survived the compiler and died the moment I actually exercised the feature.
Stay the architect. Every structural decision that turned out well — server-side tiles, separating the maire from the head-of-list in the schema, one email interface with swappable providers — came from me. Every one of those, Claude then implemented faster and more completely than I would have. That division of labor is the whole trick. Invert it and you get an app that works until it doesn’t and nobody knows why.
Write comments that explain why. My codebase is full of them, and they’re the single highest-leverage thing I did. When you come back in six weeks — or when the model reads the file fresh — the comment explaining why the maire field is separate from the list head is worth more than any amount of clean naming.
Push back. When something felt off, I said so, and about a third of the time I was the one who was wrong and learned something. Treating output as a proposal rather than an answer is the difference between a collaborator and a very fast intern you’re afraid to correct.
Was it worth it?
I’m a Tech Lead by day. I’ve shipped software for twenty years. I know roughly what ten weeks of evenings buys you, and it is not a cross-platform app with a national dataset, a tile server, an admin panel, and RGPD compliance.
What actually changed isn’t that the code got written faster — though it did. It’s that the project never hit the wall where a solo developer quits: the wall where you’re two months in, the interesting part is done, and what’s left is twelve CSV importers and a localization file. I got to keep doing the interesting part while the tedious part kept pace.
The failure modes are real and they’re specific: it doesn’t see, it doesn’t run, it doesn’t know your data, and it’s confidently wrong in ways that look right. Every one of those is survivable if you stay in the loop and actually look at your app.
VoxMap is live, free, and neutral by construction — the colors on the map are the Ministry’s official party codes, not mine. Next up: a secure citizen consultation where each vote is verified by ID, one person one voice, with no identity data retained.
Built by one person. Written with Claude. Reviewed, line by line, by me.
VoxMap is available free on iOS and Android. If you want to follow what comes next, there’s a newsletter.
