Two Ways In, One Fact
I have a website, and a native iOS app on the way. Soon they will be two ways into the same running coach, and most of the time that is a good thing.
The risk that comes with it is subtle and easy to miss. The same fact could be computed in two places. The label on a personal best, a pace written out with its units, the color on a workout card, all of these had to be produced once in Rust on the server and once in Swift on the phone. Two pieces of code that are supposed to agree, but written separately and maintained separately. Over time they drift. One gets a fix the other does not.
The App Used to Re-Derive Things
Early on, the app did some of this thinking itself. To show your weight it took the raw number and formatted it, choosing kilograms or pounds and rounding it. To label a personal best at a custom distance it built that label in Swift. To color a chat card it decided the color from the workout's kind and status.
None of that was wrong on its own. But every one of those rules already existed on the server too, because the website needed the same answers. So each rule lived in two places, and the two copies had already started to disagree. An imperial weight could round to a different pound value on the web than on the phone. A milestone node on the journey read "Test" in one place and the right word in another.
Now the Server Decides Once
I changed the direction of the work. The server computes the display value and sends it ready to show. The app reads that string and puts it on the screen without changing it. The thinking happens once, in Rust, and the phone renders the result.
The concrete cases: your weight and height now arrive as finished strings, already in your units. A personal best arrives with its label, its time, and its pace all written out for your locale. A node on your journey arrives with the exact visual it should draw and its words already prefixed, so a milestone reads "Milestone" instead of "Test". The subscription tiers, their names and their order, come from one config file the server reads. The accent color on a chat card is decided by a single function on the server and sent as a plain token the app maps straight to a color.
In each case the app deleted the code that used to work this out for itself. It does less now, and it is more correct for doing less.
A Test Fails If They Drift
Some things cannot be sent at runtime. An enum's cases, a fixed table of copy, the shape of a test fixture. For those I use a generator. I run one command, gen_ios, and it writes the Swift files that hold these shared values straight from the Rust source of truth.
Then there is a contract test. It generates those files again and compares them to the ones committed in the app, character for character. If a single character differs, the test fails and the build stops. So if I change a value in Rust and forget to regenerate, I find out at once, not after the app and the website have already disagreed in front of you.
Why This Matters to You
You'll get the same numbers and the same coaching on the website today, and on the app as soon as it is available. A personal best reads the same. A card is the same color. Your paces are written the same way.
When I fix one of these, I fix it once, and both surfaces get the fix together. And the cases that used to diverge without being noticed, imperial units, custom distances, the uncommon cases a rule has to handle, are the ones that stop diverging first, because there is now only one place where they are decided.