TL;DR: At Geonode I built the Repocket C# SDK, a single networking core that had to run on Windows, Android and iOS and handle all the logic for internet sharing across every one of them. One codebase, three very different runtimes, and a piece of software that lives in the background of someone else's device and must never misbehave. This is what shipping one C# SDK to three platforms actually taught me: where to draw the line between shared and native, why the background is the hardest place to run code, and how to keep it all honest with CI that builds on every platform, every time.
Most "cross-platform" stories are about a UI framework drawing the same button on two phones. This was not that. Repocket is an internet-sharing product, so the SDK is not a screen, it is a long-lived networking client that sits quietly on a user's machine and routes traffic. There is no interface to hide behind. If the shared core is wrong, it is wrong on Windows, Android and iOS at once, and it is wrong in the background where nobody is watching. That raises the bar for every decision.
Why C# for a job like this
The instinct for cross-platform native code is often C or Rust with thin bindings. We went with C#, and for this product it was the right call. A single managed core meant the networking logic, the part that had to be identical everywhere, lived in one place with one set of tests. On Windows it ran natively. On Android and iOS it ran through the .NET mobile runtime, so the same compiled logic drove all three platforms instead of three hand-ported copies drifting apart over time.
The rule I kept coming back to was simple: share the logic, never the platform. Anything that was pure behavior, how a connection is established, retried, throttled, torn down and reported, belonged in the shared C# core. Anything that touched the operating system, background execution, permissions, network-state callbacks, power management, belonged in a thin native layer per platform. The moment you let OS-specific assumptions leak into the shared core, you have three subtly broken platforms instead of one clean one.
The shared core, kept ruthlessly pure
The discipline that paid off most was keeping the core free of platform knowledge. The core does not know what a Windows service is or what an Android foreground notification is. It exposes a small surface: start, stop, current state, and events. Each platform's native shim is responsible for translating the messy reality of that OS into those few clean calls.
That boundary is worth obsessing over because it is where cross-platform projects rot. Every time you are tempted to write if Android do this inside shared code, you are trading a day now for a month later. I would rather add one more method to the native interface than one more platform branch to the core. It is the same instinct I bring to agent work, where I keep business logic in a separate automation layer instead of baking it into the thing that happens to be in front of the user.
The background is the hard part
A foreground app has it easy. It is on screen, the user expects it to use resources, and the OS leaves it alone. A background networking client is the opposite: every mobile OS is actively trying to suspend it, throttle it, or kill it to save battery, and it has to keep working anyway, invisibly, for hours.
This is where the three platforms stopped rhyming and each demanded its own answer.
- Windows was the friendliest. A long-running process with real background freedom, so the native layer there was mostly about lifecycle and clean startup and shutdown.
- Android wanted the work justified. Background networking has to survive Doze, app standby and aggressive OEM battery managers, which means foreground-service semantics and being an honest citizen about what you are doing and why.
- iOS was the strictest by a wide margin. The system reclaims background time hard, so the native layer had to work within iOS's rules for background networking rather than fight them, and assume it could be suspended at any moment.
The lesson that generalized far beyond this SDK: design for being paused, not just for running. The core could not assume it would execute uninterrupted. It had to hold enough state to be suspended and resumed cleanly, reconcile what changed while it was asleep, and never leave a half-open connection or a corrupt state behind when the OS pulled the rug out. Designing for the suspend is the same muscle as designing for the overlay being wrong in my AR work: assume the worst moment will happen, and make it a non-event.
Networking that has to be a good guest
Because the SDK routes traffic on someone else's device, being well-behaved was not a nice-to-have, it was the product. Bad networking behavior on a user's machine is not a bug ticket, it is an uninstall. So the core treated the host device's resources as borrowed, not owned.
That shaped the connection logic directly. Retries had to back off instead of hammering. Failures had to be contained so one bad connection never cascaded. Resource use had to stay predictable so the SDK never became the reason a user's device felt slow or hot. The networking layer's real job was less "move packets fast" and more "move packets without ever being noticed," which is a harder and more interesting constraint than raw throughput.
Getting that right once, in the shared core, meant every platform inherited the good behavior for free. That is the compounding payoff of the share-the-logic rule: solve the strict version of a problem in one place and all three runtimes get the fix at the same time.
CI that builds on every platform, every time
A cross-platform SDK is only as trustworthy as its worst-supported target, and the only way to keep three targets honest is to build all three on every change. So I set up Unity and Windows CI/CD pipelines so the whole thing compiled and packaged automatically rather than depending on whatever happened to be installed on my machine that week.
The value of that is not glamorous but it is enormous: it makes "it works on my machine" impossible to hide behind. If a change compiled on Windows but broke the mobile build, CI caught it the same hour, not three weeks later when someone tried to cut a release. For a background SDK that ships inside other apps, a broken build is not a local annoyance, it is a broken client on real users' devices, so the pipeline was as much a part of the product as the code.
I also built a React Native VPN alongside the SDK work, another exercise in the same theme: a thin cross-platform surface over deeply platform-specific system networking. VPN entitlements, tunnel lifecycle and OS network extensions are about as platform-specific as software gets, and the same discipline applied. Keep the shared logic shared, wrap the OS-specific parts in the thinnest possible native layer, and let the platform be platform-specific where it insists on being.
What building a cross-platform SDK taught me
Years later I have moved from .NET SDKs and Unity into AI agents and my own products, an arc I traced in my freelance lessons piece. But the Repocket work left instincts I use in almost everything I ship now:
- Share the logic, never the platform. One clean core plus thin native shims beats three drifting copies. The boundary is the whole game.
- Design for being paused. Code that can be suspended, killed and resumed without corruption is more robust everywhere, not just on iOS.
- Be a good guest. Software that runs where a user is not watching has to treat their resources as borrowed. Restraint is a feature.
- Build every target, every time. CI across all platforms turns "works on my machine" from a hope into a guarantee.
The tools change. I have been paid for C#, Unity, mobile SDKs, and now LLM agents, and I expect that list to keep growing. What carries across all of it is knowing exactly what a piece of software cannot afford to get wrong and building the boundary around that first. A background SDK on three platforms taught me that lesson in a place where there was no interface to hide the mistakes behind, and I have built cleaner ever since.
I build cross-platform software and AI systems for teams that need the hard parts done right, from .NET SDKs and XR platforms to voice and chat agents. If that is you, my inbox is open: more about my background here, or book a call.