NNabeel Hassan

Blog · July 17, 2026 · 8 min read

Shipping One C# SDK to Windows, Android and iOS: Lessons from Building Repocket

By Nabeel HassanAI Engineer · ICPC World Finalist

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.

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:

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.

FAQ

How do you build a single C# SDK that runs on Windows, Android and iOS?

Keep one shared C# core that holds all the pure logic (how connections are established, retried, throttled, torn down and reported) and write a thin native layer per platform for anything that touches the operating system, such as background execution, permissions and network-state callbacks. On Windows the core runs natively; on Android and iOS it runs through the .NET mobile runtime, so the same compiled logic drives all three. The rule is share the logic, never the platform: the moment OS-specific assumptions leak into the core, you get three subtly broken platforms instead of one clean one.

Why is background execution the hardest part of a cross-platform networking SDK?

Because every mobile OS actively tries to suspend, throttle or kill background work to save battery, and each does it differently. Windows gives a long-running process real freedom; Android requires foreground-service semantics and has to survive Doze and OEM battery managers; iOS reclaims background time hardest and can suspend you at any moment. The answer is to design for being paused, not just for running: the core holds enough state to be suspended and resumed cleanly, reconciles what changed while it slept, and never leaves a half-open connection or corrupt state behind.

How do you keep a cross-platform SDK from breaking on one target?

Build every target on every change with CI. A cross-platform SDK is only as trustworthy as its worst-supported platform, so pipelines that compile and package Windows and the mobile builds automatically catch a change that works on one platform but breaks another the same hour, not weeks later at release time. For a background SDK that ships inside other apps, a broken build is a broken client on real users' devices, so the pipeline is as much a part of the product as the code.

Building something in this space?

I take on AI-agent, automation and product work directly — scoped fast, shipped fast.

Book a discovery call →

Keep reading