drew galbraith

June 9, 2026

Shipping a rewrite as just another update

Prism Runner 5.0 is a full native rebuild in Swift, SpriteKit, and Metal, delivered over the air to existing players like a bug fix. How the in-place migration worked, and why the WebView had to go.

Prism Runner started life as a web game wearing an iOS app as a costume. The runtime was JavaScript, the rendering was a canvas inside a WebView, and the App Store listing politely never mentioned either. This is a fine way to ship a v1. It’s how the game got onto the store at all, and four major versions of real players, real purchases, and real reviews came out of it.

It’s also a ceiling, and I spent a long time pretending I wasn’t standing on it.

The tell was Prism Mode. The whole premise of the game is light and color. You fly a little ship between parallel dimensions, matching the color of whatever’s coming at you, and the big payoff moment, the one the game is literally named for, was supposed to feel like light splitting through a prism. What I could actually do in a WebView was cycle the hue. A rainbow color swap, standing in for refraction, in a game called Prism Runner. Nobody complained. I knew.

So version 5.0 is a rewrite: Swift, SpriteKit, Metal. The part I want to write down is the part I couldn’t find many war stories about when I went looking, which is that it shipped as a regular update. Same app, same listing. Existing players opened the App Store one day and a whole new engine came down the pipe like a bug fix.

The constraint: you don’t get a new app

The naive way to do a rewrite is to make a new Xcode project, get it perfect, and ship it as a new app. For an indie game this is roughly suicide. The App Store record is the business: the reviews, the keyword history, the purchase records, the people who already paid you. Start a new listing and all of it stays behind with the corpse of v4.

So the rule I set was to mutate the existing project in place. The bundle ID stays. The entitlements stay. The in-app purchase product IDs stay, because somebody already bought Pro and that purchase needs to restore on the new engine without them ever thinking about it. The AdMob keys stay, because the free tier is ad-supported and the accounting shouldn’t notice the engine swap either.

In practice this meant the rewrite lived inside the old app’s skeleton the entire time. Not a fresh repo I’d someday merge back. The actual project, with the web runtime carved out from under it while the container kept its identity. App Store Connect never knew anything happened. From Apple’s side, 5.0 is just a version number that’s bigger than 4.

The payoff is that launch day wasn’t a launch. No “download our new app!” migration banner, no begging players to move, no stretch where two listings cannibalize each other in search. People with the game installed got an update. Their purchases restored because nothing about the purchases changed. None of it works, though, unless you treat continuity as a hard requirement from day one instead of a cleanup task for later.

What native actually buys

It would have been enough to do the rewrite just for the input latency. A one-tap game lives or dies on the distance between your thumb and the screen, and a touch event that has to cross a JavaScript bridge always arrives slightly late, like sound from far away.

But the real list is longer, and it’s all the stuff a WebView either fakes or can’t do at all.

Prism Mode is an actual Metal effect now. Light bends through the player. The thing the name promised.

The game runs at 120fps on ProMotion displays. Haptics are Core Haptics waveforms timed to the soundtrack, instead of the fire-and-forget taps you get through a bridge.

And the accessibility work finally became possible: VoiceOver play-by-play of what’s coming, shape glyphs on the three crystal colors so color-blind players aren’t stuck guessing, Switch Control support. None of it was realistic on the hybrid stack, and all of it was sitting right there in the native frameworks.

The rewrite that redesigned the game

What nobody warned me about is that once the engine stops being the bottleneck, the game design starts looking like one.

The old world was a handful of looping backdrops, which was what the old renderer could afford. The new one travels through twelve hand-painted biomes in a single unbroken run (aurora over mountains, a crystal cave, a kelp forest, a synthwave grid) with cinematic transitions between them, and each biome carries its own music and its own signature hazard. The world changes every twenty-two seconds or so. That sounds like a marketing line, and it is one, but it’s also the actual pacing.

The combo system grew teeth. Charge gems in each color to unlock a power (Hyperdrive, Shield, Magnet), and a 10x combo still triggers Prism Mode, except now it earns the name. There’s a daily challenge where every player in the world gets the same seed and sixty seconds, and you race a ghost of your own run from yesterday, which turns out to be the most personally insulting opponent in gaming.

None of that was in the plan when I started “porting” the game. It came from the rewrite refusing to stay a port. I’d budgeted for feature parity and kept discovering that parity with a compromise is just a smaller compromise.

How it actually got built

I should be honest about the workflow. Most of the code was written by coding agents, working from spec packages I wrote up front: requirements, architecture, data models, acceptance criteria. My job looked less like typing Swift and more like what a lead does on a team. Review the architecture, interrogate the tests, send things back.

The in-place migration is exactly the kind of work where this shines, because the danger isn’t cleverness, it’s drift. Hundreds of small decisions that all have to respect the same rule, that the identity of this app cannot change, and an agent holds a rule like that across a long grind more consistently than I do at 11pm. Writing the spec is where I earned my keep. Reviewing what came back is where I earned it again.

If you’re standing on the same ceiling

The rewrite was worth it, and the in-place part was worth more than the rewrite. But the honest summary isn’t “always go native.” The WebView version had to exist first. Hybrid got the game shipped, found the players, and proved the loop was fun before I spent what a native engine costs. The mistake wasn’t starting on the wrong stack. It would have been staying there after the stack became the thing the game was about.

Five versions in, Prism Runner finally is what v1 was pretending to be. The players never had to do anything except update.

← All writing