DEV Community

Cover image for Never trust the client: 9 production lessons from 5 months building an app solo
Mykola Kondratiuk
Mykola Kondratiuk

Posted on

Never trust the client: 9 production lessons from 5 months building an app solo

I'm a solo founder. My only teammate is an AI coding agent. In five months the two of us shipped close to 40 releases of a Flutter + Supabase app that picks your dinner for you.

People assume that means the AI writes the app and I sip coffee. It's the opposite. The AI is fast hands with zero taste, so the bottleneck moved from typing to thinking — which is exactly where it should be. And thinking is where I made every one of these mistakes.

None of these are "I forgot a semicolon" mistakes. Each one shipped to production, ran quietly for a while, and taught me something I'd now tattoo on every builder. The ones that hurt most had a common shape: they never crashed. They just silently stopped the app from doing the one thing it exists to do.

Here they are, in the order they punched me.


1. Silent failures are the dangerous ones. Alert on your critical path.

The app picks your dinner using an LLM. At the time, that model was one of Google's. One day Google retired it — just shut it down, the way big companies do on a schedule you don't control.

Nothing crashed. No error on my phone, no red dashboard, no alarm. The app looked completely fine. It just silently stopped recommending anything to anyone for ten straight days.

I found out because a user messaged me: "hey, is the app broken? nothing's loading." That is the single worst way to learn your product is down — from the person you're supposed to be serving.

The bugs that crash loudly are easy; your stack traces point right at them. The dangerous ones don't break anything. They quietly stop the thing that matters while every health check stays green.

Apply it: put a synthetic check on your actual critical path, not just "is the server up." For me that's "does a recommendation request return a real dish in the last hour." Alert on the outcome your users care about, and never assume a third-party dependency will stay alive just because it was alive yesterday.


2. Never trust the client. Validate on the server, and lock down who can read each row.

My app has a premium tier. The bug: I let the phone decide whether someone was premium. The client would tell my server "hey, I'm a paying customer," and the server just... believed it. No proof.

Anyone who knew what they were doing could flip that flag and get premium free, forever.

But while fixing that, I found the scarier one underneath it: a gap in my database row-level security meant a user could, in theory, read other people's data. That's not a missing-feature bug. That's a someone-could-get-hurt bug.

I moved every entitlement check to the server (where the user can't touch it), wired premium status to the payment provider's webhook instead of the client's word, and audited every table's read policy.

Apply it: assume the user can lie to anything running on their device — the phone, the browser, the network tab — because they can. Authorization lives on the server. And "row-level security is on" is not the same as "row-level security is correct" — actually test that user A cannot read user B's rows.


3. With an LLM, every blank you leave gets filled with chaos.

Early on, my app started recommending dinner... in Chinese. To English speakers. Nobody asked for Chinese. I don't speak Chinese.

What happened: when I set up the model prompt, I never explicitly told it what language to respond in. And an LLM doesn't leave a blank blank — it confidently fills it with whatever it feels like. So it picked a language. Sometimes Chinese. Sometimes who-knows.

The fix was one line: respond in English.

This is the whole job of prompting in production. The model is not reading your mind; it's pattern-matching into the gaps you left. (Same model, months later, taught me the sequel to this lesson: today's "thinking" models burn hidden reasoning tokens against your output budget, so if you don't explicitly turn that down, your responses silently truncate. Another blank, another surprise.)

Apply it: be painfully explicit. Language, format, length, tone, what to do when it's unsure — pin all of it. Then feed the model garbage and adversarial inputs in testing and watch what it does with the gaps, because your users will find them.


4. Monitor your money path harder than anything else.

Someone actually wanted to pay me. They used the free app, liked it, tapped "subscribe" with their wallet open... and my paywall showed them a blank screen. No prices. No button. Nothing.

Why: on startup the app asked the store for my subscription prices, that request quietly failed, and the code never retried. So for some users, the single most important screen in the entire business was broken and showing nothing.

I had no idea. Nobody emails you "I tried to give you money and couldn't." They just leave. I added retries and monitoring on exactly that moment and fixed it — but I will never know how many people I lost first.

Apply it: instrument the moment someone tries to pay you more heavily than any other event in your app. Track "paywall rendered with prices" as an explicit success metric, alert when it dips, and never let a one-shot network call guard your revenue without a retry. Silent revenue loss is the most expensive bug there is, precisely because it never shows up as a bug.


5. Ship lean. Every unused permission, library, and line is a liability.

Apple rejected my app. Three reasons, all useful if you're about to submit:

  1. My subscription products weren't configured to match exactly across App Store Connect and my code (prices and terms have to line up perfectly).
  2. I needed a public refund-policy page online before they'd approve a paid app.
  3. My favorite: they flagged me for location and tracking code I wasn't even using. It was dead code, left over from an abandoned idea, just sitting there. Apple saw the API references, assumed I was tracking people, and said no.

I ripped the dead code out, fixed the subscription config, published the policy page, resubmitted, got in.

Apply it: every permission you declare, every SDK you link, every line you keep is something you have to defend — to a reviewer, to a security audit, to your future self. Delete what you don't use before someone else makes you. Rejection isn't failure; it's a checklist you didn't know existed.


6. Treat your AI model as a swappable commodity. Don't marry a provider.

The model that picks dinner used to be one provider's. It worked, but it was a little slow and a little expensive — and for an app whose entire magic is speed, slow is death.

I swapped it for a different model from a different company in one afternoon. Same app, same features, recommendations come back about 42% faster, and it costs less to run.

The only reason that was an afternoon and not a rewrite is that I'd put the model behind a thin proxy layer with the model ID in exactly one place in my code. New models ship every few months, each cheaper or faster than the last. If your provider is welded into 40 call sites, you can't take that deal.

Apply it: put one seam between your app and any LLM (a proxy function, an interface, whatever). Keep the model name in a single constant. Then "the new model is 2x cheaper" becomes a config change instead of a project. Bonus: nobody ever posts a screenshot of "42% faster," but your users feel invisible wins even when they can't name them.


7. Subtraction is a feature. Deleting code is the work, not a break from it.

My favorite thing I did all month wasn't building a feature. It was deleting 2,012 lines of my own code.

Old experiments I never finished. Clever solutions to problems I no longer had. Three different ways of doing the same thing. I ripped it all out, and the app does exactly what it did before — same features, same speed, just less surface area for bugs to hide in.

Every line you keep is a line you maintain, debug, and carry forever. Less code is less liability. This isn't a coding quirk — writers cut paragraphs, designers remove elements. The hard, valuable skill in anything creative is the nerve to remove what doesn't earn its place.

Apply it: schedule deletion like you schedule features. If a code path hasn't justified itself in months, it's not an asset, it's debt with a nice haircut.


8. Make reversible bets, and have the nerve to actually reverse them.

I once shipped a big new push across my marketing site at 9am, felt great, made coffee — then looked at the site like a stranger would and realized it muddied the one clear message I actually wanted people to get. It wasn't bad. It was wrong for right now.

I reverted the whole thing the same day. Hours of work, undone.

Shipping fast gets all the hype; knowing when to un-ship fast is just as important and almost never talked about. The trap is sunk cost — "I worked hard on this, so I have to keep it." But the work is gone either way. The only question left is whether keeping it makes the product better.

Apply it: prefer changes you can roll back cleanly (feature flags, small PRs, decoupled deploys). Then judge a shipped change on the product as it is now, not on the effort you already spent. Don't fall in love with your own code.


9. Most of building is invisible plumbing that has to be perfect. That's the job, not a detour.

A few of the least glamorous things I did, that no user will ever thank me for:

  • Migrated the payment system twice in one week to get web payments, taxes, refunds, and multi-country handling actually correct. Two full rewrites of the most boring, most critical part of the app in seven days.
  • Three months on SEO — pages, tiny tags, redirects, structured data, the same post rewritten four times for one keyword. Zero dopamine. But you can build the best app in the world and if nobody can find it, it doesn't exist. A great product nobody discovers isn't a product, it's a secret.
  • Spent a day tracing one broken deploy to a transitive dependency three levels deep that I never directly installed, which quietly became incompatible with my web build and took down both of my sites. My code was fine. The bug was, as always, not where I thought it was.

The build-in-public highlight reels are all shiny features and launch-day champagne. The actual job is mostly this: the unglamorous 80% that has to work before anyone sees the 20%.

Apply it: if it feels like you're grinding on infra, taxes, edge cases, and distribution instead of "real progress" — you're not behind. That is the work. Pin your dependency versions, read what's actually in your node_modules/pubspec, and treat distribution as half the product, not an afterthought.


Two more that aren't bugs, but cost the most to learn

Honesty compounds; fake social proof spends trust. When I launched, every template screamed "add testimonials, look bigger." So I dropped placeholder reviews and an inflated user count on my own site, told myself I'd fix it later, and felt a little gross every time I opened it. Eventually I deleted all of it and put my real, much smaller numbers up. People can smell fake — and the moment someone catches one fake number, they stop believing the true ones too. Small and honest beats big and fake, because honesty is the one thing a tiny app can offer that the giants usually won't.

Don't assume your users are like you. I'm Ukrainian; I think in English when I code, so I launched English-only. Most people on Earth aren't browsing in English. The app now speaks 15 languages, and not the lazy way where only the buttons translate — it rewrites the actual dish names and recipes live. The most expensive assumption you can make as a builder is that your users share your language, your phone, and your life. They don't.

I also gave users less for free on purpose (tightened the daily free limit) and ripped out all the tracking — no ad SDKs, no location, no cross-app cookies — because I kept asking "do I actually need this to make the app good?" and the answer was almost always no. Free can't be infinite or the thing dies and helps no one; and trust is worth more than data when you're asking people to tell you what they eat every day.


The honest scoreboard

In five months, solo, with an AI as my only teammate, I:

  • let a third party silently kill my app's brain for 10 days
  • shipped a security hole that gave away premium (and nearly leaked user data)
  • got rejected by Apple over code I forgot to delete
  • showed a blank paywall to someone who wanted to pay
  • deleted features I was most excited to build because testing said no one wanted them

That's a lot of mistakes for one person. And every one is in this post, because I'd rather show you the real thing than a polished one.

Here's what's underneath all of it: building isn't a straight line from idea to launch. It's a loop — ship, watch it break, fix it, forever. The people who "make it" aren't the ones who avoid mistakes. They're the ones who show up the next morning after each one.


What I'm building

The app is SomeYum — it takes the nightly "what do I eat today?" decision off your plate. You swipe through a handful of dishes (not 200), say yes to one, and get the recipe. It learns your taste from what you actually swipe, speaks 15 languages, and there's a literal spin-the-wheel panic button for when you want zero choices. Free to use; premium is $4.99/mo or $29.99/yr with no trial games.

If you're building something solo, especially with an AI in the loop: which of these have you already hit? I'm collecting war stories, and the comments here are where the best ones live. 👇

Top comments (0)