One Tuesday at Kikoff
We merged a small visual fix on a Tuesday morning. We built a patch that afternoon. By Tuesday evening, the fix was on our users' phones. No app store submission, no "please update your app" prompt, no week-long wait.
For most of Kikoff's life as a mobile app, that wasn't possible. A one-line bug fix took the same path as a major release: build a new version of the app, submit it to Apple and Google, wait 1–3 days for review, then wait weeks for users to actually install the update. We were paying a tax on every change. We knew we were paying it, and we suspected most other mobile teams were too.
This is the story of how we stopped, why we could only stop recently, and why most mobile teams are still paying by default.
The web figured this out fifteen years ago
When a website has a bug, here's what happens: an engineer writes a fix, clicks a button to deploy it, and boom! The next time you refresh the page in your browser, you're running the fixed version. From "we know how to fix it" to "our users have the fix" in seconds.
That's been true for roughly fifteen years. It wasn't always. Before about 2010, deploying a website looked a lot like releasing a mobile app today: scheduled, manual, risky, often happening at 2am on a Sunday. An engineer would log into a production server, copy files up by hand, restart the web server, and hope nothing broke. A fix took hours to days. Rollbacks meant another manual deploy.
Three things changed it:
- Cloud infrastructure arrived. Before the mid-2000s, running a website meant owning or renting physical servers in a data center. Adding capacity took weeks. Amazon launched AWS in 2006, and suddenly spinning up a server was an API call. Weeks became minutes. If your deploy broke, you could spin up a fresh server with the old version and swap it in. Servers went from being expensive pets you cared for to cheap livestock you replaced.
- Continuous integration matured. Tools like Jenkins, Git, and GitHub made it normal to automatically build and test every change an engineer made. Before CI, testing was a manual step most teams skipped or did once at release. After CI, every commit ran through a test suite before it could ship. That made shipping quickly feel safe instead of reckless.
- Culture caught up. In 2009, Flickr gave a famous talk titled "10+ Deploys Per Day." Etsy and GitHub wrote blog posts bragging about how often they shipped. Within a few years, "we deploy ten times a day" went from unhinged to table stakes.
The tools were in place by around 2006. The culture took a few more years to catch up. By about 2010, continuous deployment had a name, a following, and a growing list of companies doing it in the open.
There's a deeper reason this worked for web and not mobile. The web has no gatekeeper. When you update your server, the next browser to ask for a page gets the new version. No platform owner approved your change, because there is no platform owner. That isn't a tooling decision, it's the architecture of the web itself.
Mobile is different. Apple and Google own the phone's front door. Anything that runs on a user's phone got there through one of their stores, which means anything that changes has to go back through those stores. That's not something better tooling can remove. It's structural.
Here's what that looks like in practice. Between we fixed the bug and our users see the fix sits a gauntlet:
- The team builds a new version of the app. This is a single packaged file (called a "binary") that phones will install.
- That file goes to Apple and Google for review.
- Apple and Google take 1–3 days to approve it.
- Users have to actually install the update, which trickles out over days or weeks.
Even when everything goes smoothly, it takes days before most of your users are running the new version. A real hotfix, the "we broke something, we need to fix it right now" kind, still takes a week before it's meaningfully in the field.
Every mobile team pays this cost. Most of them don't realize they're paying it. We call it the store-review tax.
Not everyone pays the same rate
How much the tax hurts depends on what your team does about it.
Big teams solved it by leaving the phone behind
Uber, Airbnb, Cash App, DoorDash: the apps on your home screen. They've mostly made the tax irrelevant. They didn't get Apple and Google to speed up review. Instead, they moved most of their product out of the app on your phone.
It works like this. Normally, most of what you see in an app (the buttons, the text, the screens) is baked into the app file on your phone. If the company wants to change it, they have to ship a new version of the app.
Big companies flipped this around. The app on the phone is a near-empty shell that asks a server, "what should I show right now?" The server answers with the layout, the text, the logic. Want to change a button's color? The company changes one line on the server. Your phone picks up the change the next time it talks to that server, which is constantly.
Airbnb wrote about their version of this in 2021. Internally they call it the "Ghost Platform," a nod to the Guest and Host sides of their apps. It's a single shared system that defines screens and user interactions on the server, with matching native renderers on iPhone, Android, and their website. A majority of Airbnb's most-used features run on it: search, listing pages, checkout. These are the screens every Airbnb user sees, and Airbnb's product teams can iterate on them without every change waiting on a new app release.
This works beautifully. It also costs tens of engineers and years of building before it's worth it. Not every team can afford it.
For a decade, only half of cross-platform had a shortcut
Most smaller mobile teams can't afford to engineer their way around the store. For roughly the past decade, though, one category of mobile team has had a shortcut: teams that build their app once for both iPhone and Android using a "cross-platform" toolkit, most commonly React Native (RN) or Flutter.
One of these two toolkits, React Native, came with a trick built in. Because of how React Native apps are constructed internally, the toolkit's maker (Microsoft) figured out how to ship bug fixes directly to users' phones, bypassing the app stores entirely. A move somewhere between clever engineering and politely ignoring Apple's preferences. They called it code push. Since 2015, React Native teams have effectively been able to fix bugs and have the fix on everyone's phone within a day. No store review, no update tap.
For years, the other major toolkit, Flutter, built by Google, couldn't do this. Flutter apps are constructed in a way that makes them faster and smoother than React Native apps, but that same internal design made code push seemingly impossible.
Then 2023 happened
In 2023, a small company called Shorebird figured out how to make code push work for Flutter anyway. It was genuinely hard engineering, the kind most Flutter teams had quietly given up on, and it closed a gap that had existed for almost a decade.
The result: Flutter teams now get the same "fix a bug, land it on everyone's phone today" loop that React Native teams have had since 2015, without giving up the performance that made them choose Flutter in the first place.
Why the review gate exists
The store-review tax isn't arbitrary. Apple and Google gate updates because apps can do real harm: malware, scams, private-API abuse, undisclosed data collection. The review catches some of that. It's also the mechanism by which the platforms enforce their rules against deceptive user experiences and content policy violations.
Code push exists inside that system, not around it. Both CodePush and Shorebird are legal under Apple's and Google's rules because they only ship the same kind of code the app was originally approved to run. You can fix a bug or tweak a button. You can't smuggle in a gambling minigame. Teams that try get their app pulled, which is the worst tax of all.
Not a ladder, a matrix
It's tempting to line all of this up as a linear progression: worse-to-better. It's also wrong. There are two separate decisions.
The first is what toolkit you use to build the app. If you're going cross-platform, that's mostly a choice between React Native and Flutter, and the tradeoffs are real.
Neither is strictly better. They fit different teams and products.
The second decision is what you do about the store-review tax, and it's independent of the first. You can do nothing, you can adopt code push, or you can build your product to run mostly on a server. The cost and the scale at which each makes sense are very different.
Every combination is a valid choice. The question is whether you picked it on purpose or ended up there by default.
Code push has a floor, and it's your users
For Flutter teams specifically, there's a limit that React Native teams don't have. It's the one place React Native gets to feel a little smug.
Because of how deeply Flutter's code gets baked into the app binary, applying a patched version requires the app to fully close and reopen. Not just switch away and come back. That moment, when the app starts fresh, is called a cold start. Until a user cold-starts the app, they're still running the old version.
React Native apps don't work this way. Because React Native's code is loaded live while the app runs, its code-push system can swap in a new version mid-session or the moment a user switches back from another app. No full restart required. It's one of the flexibility advantages of the React Native side of the matrix.
<aside>📊
For Flutter + Shorebird, how fast a fix reaches users is bounded by how often those users fully reopen the app. Code push already strips away store review and the need to install a new binary. What's left is the user's own habit of closing and reopening.
</aside>
Your tooling can't change that habit. What it can do is nudge. When a patch is downloaded and ready, we show a small banner asking the user to restart. It's a much lighter ask than a store update (seconds, not minutes, no installer), and it measurably pulls the cold-start curve in.
We could go further. A full-screen takeover, a forced restart, a blocking dialog. They'd all work. We don't, because the restart is a favor we're asking, not a toll we're collecting. A gentle nudge respects that.
.png)
Traditional store releases can sit far below all the code-push curves, because they stack review delay and "please tap update" friction on top of the reopen cadence. Code push removes those extra layers. For Flutter, it can't remove the reopen requirement itself.
The only thing that can get below the cold-start floor is the server-driven approach big companies use. When your server is deciding what the app shows, the fix arrives the next time the app talks to the server (constantly), not the next time the user fully restarts it.
For most Flutter teams, that server-driven approach is overkill. Code push is the right stopping point, even with the cold-start catch.
Back to that Tuesday
Remember the Tuesday at the top of this piece? That's the loop we bought ourselves. Merged in the morning, on users' phones by evening, with a gentle "restart the app to apply" banner doing the last mile. By Tuesday night, most eligible users were on the fix.
That loop, merged Tuesday morning, live Tuesday evening, is what web teams have taken for granted since 2010. We didn't invent it. We just picked the cell in the matrix that fit our team size and our product, and stopped paying the store-review tax on the changes that make up most of our work.
We can see the next stage from here
Here at Kikoff, we're at the far end of where code push is exactly the right tool. We're big enough that waiting on app store review for every small bug was a real drag on the business; we're not big enough (yet!) to invest building the server-driven infrastructure Uber and Airbnb have.
That'll change. As we grow, more of our app will move to the server side of the line, so that changes can reach users the next time the app talks to our servers, rather than the next time a user fully reopens the app. It's the natural next step, and we can see the shape of it in our own data. We're not there yet. But we can see it from here, which, as it turns out, is the best view you get before you have to build it.
<aside>💡
How fast your fixes reach users is a design choice, not a fact of life. Big companies engineer around the store-review tax with server infrastructure. Smaller cross-platform teams can now sidestep it with code push. Teams who do neither are paying the tax by default. Fine if it's deliberate, a quiet drag if it isn't.
</aside>
Pick the cell in the matrix that matches your scale and your product. Don't build Uber's stack if you're not Uber. Don't accept week-long bug fixes if you don't have to. And know that picking between React Native and Flutter is a flexibility-vs-performance call, not a delivery-speed call anymore.
We stopped paying the store-review tax on the changes that make up most of our work… and your team can too.
About the Authors










.png)

