I build mobile apps for a living, mostly on iOS. This year I shipped a VPN called Privalune on iPhone, iPad, Mac, Android and Windows. I did it alone, and the only reason that was possible is one decision I made early: I would not write or bundle a VPN protocol.
Every platform I target already ships an IKEv2 client. So the apps are remote controls. They hand the operating system a server and a login, and the OS runs the tunnel.
This post covers how that works on each platform, what the backend looks like, and where the approach hurts.
The numbers
iOS, iPadOS and Mac: about 7,400 lines of Swift (SwiftUI)
Android: about 4,200 lines of Kotlin (Jetpack Compose)
Windows: about 3,350 lines of TypeScript, CSS and Rust (Tauri 2 + Preact)
Backend: seven Supabase Edge Functions and five SQL migrations
Website: static HTML, 92 pages in six languages
None of those codebases contains cryptography or packet handling.
iOS and Mac: NEVPNManager
On Apple platforms the system VPN is NEVPNManager, the "Personal VPN" API. You fill in an IKEv2 profile, save it, and ask the system to start it. iOS shows its own permission prompt the first time.
The part I like most is on-demand rules. This is the whole "protect me on public Wi-Fi automatically" feature:
enum AutoProtect { static func rules(trustedNetworks: [String]) -> [NEOnDemandRule] { var rules: [NEOnDemandRule] = [] if !trustedNetworks.isEmpty { let stayOff = NEOnDemandRuleDisconnect() stayOff.interfaceTypeMatch = .wiFi stayOff.ssidMatch = trustedNetworks rules.append(stayOff) } let connectOnWiFi = NEOnDemandRuleConnect() connectOnWiFi.interfaceTypeMatch = .wiFi rules.append(connectOnWiFi) return rules } } The system evaluates those rules even when the app is not running. I wrote no background code for it.
Two things I learned the hard way:
The Simulator cannot run a VPN. I added a simulated tunnel with realistic timing so I could build the whole UI, including the connect animation and the Live Activity, without a device in my hand.
Mac came almost free. The iPhone and iPad app runs on Apple silicon Macs through the App Store, so Mac users get the same binary. Intel Macs are not covered.
Android: the platform VPN profile
Most Android VPN apps implement VpnService and ship their own protocol stack. Since Android 11 there is another option: Ikev2VpnProfile with VpnManager. You build a profile, the system asks the user for consent once, and Android runs the tunnel.
The flow is: prepare() saves the profile and may return an Intent for the consent screen, then start() brings the tunnel up. To know the real state, I listen for networks with TRANSPORT_VPN instead of trusting my own flags.
The cost is a minimum of Android 11. For a new app in 2026 I accepted that.
Windows: Tauri, PowerShell and the RAS API
The Windows app is Tauri 2 with a Preact front end. The Rust side is about 340 lines and does three things:
Creates a per-user VPN profile with PowerShell's Add-VpnConnection. Per-user means no administrator prompt.
Dials and hangs up through the Win32 RAS API, using the windows crate. The password goes straight into the API call, so it never appears on a command line where other processes could read it.
Translates Windows error codes into sentences a person can act on:
let message = match code { 691 => "The VPN login was refused. Please try again.", 13801 => "Windows couldn't verify the server's certificate. Please try another location.", 809 | 13806 | 13820 => "This network seems to block VPN connections. Try another Wi-Fi or mobile hotspot.", 868 => "The server's address couldn't be found. Check your internet connection.", _ => "Couldn't connect. Please try again or pick another location.", }; I do not own a Windows PC. A friend ran the first builds for me, and I tested it myself later. That made the error table far more important than I expected: "Error 809" in a screenshot told me exactly what happened on his network.
The backend: small functions, one source of truth
The backend is Supabase Edge Functions, written in TypeScript for Deno:
servers returns the location list. It is cached for 30 minutes, and if the upstream call fails it serves the last good list instead of an error.
vpn-credentials checks that the caller has Premium and returns a VPN login.
devices links up to 10 devices to one subscription.
A webhook keeps subscription state in sync with RevenueCat, which is my single source of truth for "is this person Premium" across Apple, Google and web purchases.
On infrastructure, to be clear about what I did and did not build: the free server is a VPS I configured as a VPN server myself. It meters the daily free time on the server side, and the app only mirrors the countdown. The paid locations come from an infrastructure provider's network through their API. I did not rack servers in 55 countries.
No accounts in the apps
I did not want a sign-up screen between a person and the connect button. So the apps have no login.
Someone who buys on the website receives a Premium key by email. Typing that key into any of the apps links the device, up to 10 per subscription. Each linked device gets a random token, and the database stores only its hash.
The trade-off is support. With no account, "I lost my key" has to be solved by email, so the website has a small dashboard where buyers sign in with a one-time code and see their key again.
The website is plain HTML
No framework. A shell script copies files into dist/ for Cloudflare Pages. Small Python scripts generate the repetitive pages, and translations live in one JSON file per language, keyed by text segment, so changing an English sentence tells me exactly which translations are stale.
One environment variable, LAUNCHED, switched the site from a holding page to the full site on launch day.
What this approach costs
One protocol. IKEv2 only. Some networks block it, and I cannot fall back to something that looks like normal web traffic.
Less control. I get the features each OS exposes and nothing more.
Windows trust. My installer is not code-signed yet, so Windows warns people before running it. That is the next thing I am fixing.
Android 11 and later only.
What I got in return: four small apps that one person can maintain, no native crypto to audit, and tunnels run by code that Apple, Google and Microsoft maintain.
Try it
Privalune is free to start with no account: privalune.com. If you only want to see what websites can read about your connection, the What's my IP page works without installing anything.
I would like to hear from anyone who has shipped on the platform IKEv2 clients, especially about handling networks that block it.