The pitch for Flutter is that you write one codebase and it runs on iOS, Android, Web, macOS, Linux, and Windows. I've shipped two apps on that promise — PokiPaw, a pet-care app live on the App Store and Google Play, and Admitere Drept, a law-exam simulator also live on both stores — and the honest version is more interesting than the slogan. "Six platforms" is true in the sense that the project compiles and runs in six places. It is not true in the sense that you have six shippable products. The gap between those two statements is the whole subject of this article.
Both apps are Dart and Flutter, both manage state with Provider, both monetize through native in-app purchases, and they make opposite backend choices. That contrast is useful, so I'll use them as the two data points throughout.
Real targets versus nominal targets
PokiPaw's pubspec targets all six platforms. What actually ships and gets used is iOS and Android. The desktop and web builds exist, they compile, they run — but they're closer to "compiles and runs" than "polished and supported." Linux and Windows in particular are surface area I keep alive, not products I'd point a stranger at. Admitere Drept is even more explicit about this: iOS and Android are shipping, and desktop and web are scaffolds sitting in the tree.
That distinction — shipping versus scaffold — is the one I wish someone had drawn for me earlier. Flutter makes the nominal target list cheap. Adding windows or linux to the build matrix is a flag. Making either one good is a separate project with its own input model, window management, file-picker behavior, and store (or no store at all, in the case of Linux). The codebase being capable of six platforms is real engineering value. It is not the same as having shipped six.
So when I say PokiPaw is "on six platforms," I mean the codebase is. The product is on two. I try never to blur those, because that blur is exactly how cross-platform claims become marketing fiction.
Where "write once" actually holds
The good news is that the part Flutter is sold on is genuinely true: business logic, data models, networking, state, and the bulk of the UI are write-once. In PokiPaw the health hub — medical records, vaccine calendar, dose-by-dose medication tracking, the breed-specific scoring — is the same code everywhere. In Admitere Drept the exam engine — 45 questions, the countdown timer, category stats, the final-average calculator — is one implementation.
My rough rule after these two apps: about 85% of the work is shared, and the last 15% is per-platform. That 15% is not evenly distributed, either. It clusters around the things that touch the OS: notifications, purchases, sign-in, biometric locks, file pickers, deep links. The closer a feature sits to the platform, the more it leaks platform-specific handling, and the more "write once" turns into a polite fiction for that last slice of each target's UI and integration.
Backend: Supabase for one, Firebase for the other
The two apps diverge most clearly on the backend, and the reasons are instructive because they weren't arbitrary.
PokiPaw runs on Supabase end to end — Postgres with row-level security, Storage, Auth, Realtime, and Edge Functions. The driving requirement was real-time family sharing with full write access: multiple people in a household editing the same pet's data, with every row scoped to the right family. Postgres plus RLS plus Realtime is a clean fit for that. RLS lets the database itself enforce that a member only ever touches their own household's rows, which is the kind of guarantee I want living in the data layer, not in app code I might get wrong.
Admitere Drept runs on Firebase — Auth, Firestore, and Cloud Functions. The shape of that product is different. It isn't a multi-writer collaborative app; it's a single student practicing tests, with a separate React admin console (built via Lovable) doing question CRUD, CSV import, and reports triage against the same backend. Firestore's document model and Firebase's batteries-included Auth and Functions matched a read-heavy, single-user-per-account workload, and the admin tooling ecosystem around Firebase made the back-office side faster to assemble.
The lesson isn't "Supabase good, Firebase good." It's that the backend should follow the data's social shape. Collaborative, relational, row-scoped writes pulled PokiPaw toward Postgres and RLS. Single-user practice with a bolt-on admin console pulled Admitere Drept toward Firestore. Flutter on the front end didn't care either way, which is its own quiet argument for the framework: it doesn't drag a backend opinion in with it.
State management: Provider in both
Both apps use Provider. Not Riverpod, not Bloc, not the newest thing. Provider is boring, well-understood, and good enough for apps of this size, and "boring and good enough" is a feature when you're one person maintaining a portfolio of products. There's a real cost to picking a different state solution per app: every context switch between projects is a tax. Standardizing on Provider means the mental model is the same whether I'm in the pet app or the exam app, and that consistency is worth more to me than squeezing out a marginally cleaner architecture in one of them.
Monetization plumbing across two stores
Both apps make money through the stores, and this is where cross-platform stops being about UI and starts being about each platform's rules.
PokiPaw is freemium: free core, Premium at $2.99/month, Family Seats from $1.99 to $5.99/month, on native in-app purchases. Admitere Drept is freemium too but mixes models — a free tier with ads via google_mobile_ads (AdMob), and a one-time Pro unlock at 19.99 Lei through in_app_purchase that removes ads and unlocks unlimited tests and full explanations.
The plumbing here is deceptively heavy. in_app_purchase gives you one Dart API, but behind it sit two completely separate billing systems — App Store and Play Billing — each with its own product configuration, sandbox testing, receipt validation, and review expectations. AdMob adds a second integration with its own consent, mediation, and platform quirks. None of this is hard, exactly; it's just that "wire up IAP" is really "wire up IAP twice, plus ads, plus test both sandboxes, plus handle the restore-purchases edge cases." Flutter unifies the call site. It does not unify the stores behind it.
Store submission as a solo dev
The last mile is the one no framework saves you from. Two apps means two App Store entries and two Play entries — com.trinxsolutions.pokipaw and com.rdmsolutions.admiteredrept — each with its own metadata, screenshots, privacy declarations, age ratings, and review cycle. PokiPaw is at v1.2.6+26, Admitere Drept at v1.0.16+16, and every one of those build numbers is a round trip through two review processes.
Six platforms makes that worse in principle, which is the real reason I keep desktop and web as scaffolds. Each shipping target is a maintenance tax: another store (or distribution mechanism), another set of OS updates to chase, another surface that breaks when Flutter bumps a major version. As a solo dev, the rational move is to ship the two platforms where the users are, keep the other four buildable, and not pretend the scaffolds are products.
What I'd tell you before you start
Flutter delivers on the part that matters: one codebase genuinely carries the logic and most of the UI across targets, and it stays neutral about your backend so you can pick Supabase or Firebase on the merits. What it does not do is make "six platforms" free. Each real target costs you store work, platform integration, and ongoing maintenance, and the honest play for most solo projects is two shipping platforms with the rest scaffolded.
If you want to see how this played out in practice, the PokiPaw write-up covers the Supabase real-time side and the six-platform maintenance tax, and the Admitere Drept write-up covers the Firebase, AdMob, and admin-console side of the same framework choice. Both are part of the larger story of building 11 products as a solo founder. If you're weighing Flutter for something that has to be in a few places at once, the projects page shows where it's earned its keep.