Lessons from building a small VPN service around standard WireGuard clients instead of a proprietary app
When you look at a commercial VPN product, the app seems to be the product: a polished interface, a country list, and a large Connect button. I chose the opposite approach. Instead of building another VPN client, I decided to give users a standard WireGuard configuration that they could import into an existing client. That decision removed a lot of client-side work — but it also exposed where the real complexity of a VPN service actually lives.
Why build another app if WireGuard already has one?
The usual commercial VPN flow is straightforward: install the vendor's app, sign in, choose a location, and connect. A proprietary client can manage server selection, subscriptions, kill switches, automatic reconnects, diagnostics, updates, and support in one place.
But for a small service with one or a few locations, I had to ask a more basic question: do I really need to build and maintain a separate Windows, macOS, Android, and iOS client just to establish a WireGuard tunnel?
WireGuard already has mature clients across the major desktop and mobile platforms. A user can import a configuration file or scan a QR code and get a normal VPN toggle. On paper, that looked like a very attractive tradeoff: less client code, fewer update mechanisms, fewer installers, and a smaller attack surface to maintain.
What I underestimated was that the app was never going to be the hardest part.
A .conf file is not just a settings file
The first architectural lesson was simple but important: a WireGuard configuration is effectively a credential. It contains the client's private key. A QR code that represents the same configuration contains the same sensitive material in another form.
That immediately creates product problems that have nothing to do with the tunnel itself. How do you show the configuration safely? What happens if the user loses it? Can you issue a replacement without leaving the old peer active? How do you revoke access reliably? What should happen when a user creates more than one configuration?
Once payments are involved, the configuration also becomes part of a lifecycle. It may be pending, active, expired, replaced, revoked, or associated with a failed payment. A payment provider can deliver callbacks asynchronously, and the moment a peer is added to the VPN host, that is real network access — not just a row in a database.
The supposedly simple flow of “generate a config and show a QR code” quickly turns into a small state machine.
WireGuard is simple. The service around it is not.
One thing I like about WireGuard is that it stays focused. It creates an encrypted tunnel between cryptographic identities and works with routing. It does not try to solve subscriptions, account recovery, customer support, payment state, key distribution, or user experience.
That separation is good architecture, but it can be misleading during early prototyping. You can bring up a working tunnel very quickly and feel as if most of the project is done. In my case, the tunnel was the easy part. Making the surrounding system behave predictably was the real work.
The user should never need to know that there is a VPN host, a web service, a database, a configuration generator, and asynchronous payment callbacks behind the scenes. Their mental model should stay simple: payment succeeds, access works; payment fails, access is not active; renewal succeeds, nothing else is required.
“Connected” is not the same as “configured correctly”
Another group of problems appeared around routing and leaks. A client can display “Connected” while the overall network behavior is still wrong. The public IPv4 address may change while DNS requests still go somewhere unexpected. Or IPv4 may use the tunnel while IPv6 leaves through a direct route.
That changed the way I test the service. A successful WireGuard handshake is necessary, but it is not sufficient. I also check the public address, DNS behavior, IPv6 behavior, and actual routes on the client.
Different operating systems, home routers, and mobile networks can behave differently here. In my current setup, IPv6 is intentionally not used on the client side, so one practical check is making sure the client does not have an alternative IPv6 path to the Internet. That is not a universal VPN design rule — it is an example of why tunnel status and full network correctness are two different things.
The nastiest failures were not cryptographic
Before building this, I expected most of the difficult work to be low-level networking. Some of the most annoying bugs turned out to be ordinary distributed-systems and operations problems.
A service restarts, but the API is not ready yet. A health check fires once, gets a connection failure, and reports the whole deployment as broken even though the process is still starting normally.
A payment provider retries the same callback. The operation must be idempotent, or the same event may create duplicate access or extend a subscription twice.
The database says a configuration is revoked, but an old peer is still present on the VPN host. From the user's point of view, the system is still wrong.
These are the kinds of issues that rarely appear in a “WireGuard in 15 minutes” demo. They only appear when the system has to survive restarts, retries, network failures, partial failures, and real user behavior without manual cleanup.
What I gave up by not building a proprietary client
There are real tradeoffs. A proprietary VPN client can hide the configuration file entirely, switch servers automatically, expose subscription state, implement a kill switch, collect diagnostics with user consent, and provide the same interface on every platform.
It can also make infrastructure changes easier. If an endpoint or server key changes, an app can fetch updated settings automatically. With a static WireGuard configuration, an incompatible server-side change may require issuing a new config.
So I do not think proprietary VPN apps are pointless. For a large service with many locations, automatic routing, split tunneling, advanced policy controls, and complex support requirements, a custom client makes a lot of sense.
My conclusion is narrower: for an early-stage WireGuard service, a custom VPN app does not have to be the first major component you build.
What the standard-client approach gave me
The biggest benefit was a dramatic reduction in client-side code. I do not need to maintain separate custom clients for Windows, Android, iOS, and macOS. I also avoid maintaining my own update mechanism, installers, signing pipeline, and a whole additional UI failure surface.
The second benefit is transparency. The user receives a standard WireGuard configuration and can use it with a standard client. The service is not forcing them into a proprietary interface just to establish the tunnel.
The third benefit is focus. Instead of spending time recreating a Connect button, I could work on the parts that actually determine service reliability: access issuance, peer revocation, post-change verification, backups, logging, and correct behavior around failed payments.
What I would design first if I started again
Define every access state before writing the UI. Write down what “new,” “active,” “payment failed,” “expired,” “revoked,” and “replaced” actually mean. A clear state model prevents a surprising amount of rework.
Make external callbacks idempotent from day one. Payment and webhook events can be retried. The same event must not create a second access grant or extend the same subscription twice.
Treat database state and VPN-host state as separate realities. A correct database record does not prove that a peer was actually added or removed. Both sides need verification.
Do not trust a single health check immediately after restart. A healthy service may not be ready to accept requests yet. A short readiness loop is better than one impatient request.
Design recovery before you need it. A backup is not just a file that exists somewhere. You need to know what to restore, in what order, and how to verify that the restored system is actually consistent.
Teach users that the QR code is a credential. A QR code feels like a convenient picture, but in this context it represents access material and should not be published or forwarded casually.
So, does a VPN need its own app?
My answer now is: it depends on scale and product requirements. If you need many locations, seamless server switching, advanced routing, client-side policy features, and a tightly controlled support experience, then a custom app is justified.
But if the core product is a standard WireGuard tunnel, you can start without reinventing the client. The existing WireGuard apps already solve the core client-side task: bringing up the tunnel.
The time saved can be spent on the parts users barely see but depend on completely: key issuance, access lifecycle, routing, DNS, failure handling, backups, and recovery.
The paradox is that skipping a custom app did not make the VPN service “simple.” It removed one large layer and made the real complexity visible much sooner.
Project context
These lessons came from building HVPN, a small Ukrainian VPN service based on WireGuard and standard client configuration rather than a mandatory proprietary app.
The project is still young, so I am treating these as engineering lessons from a real implementation rather than universal rules for every VPN architecture.
References
WireGuard official website: https://www.wireguard.com/
WireGuard installation page: https://www.wireguard.com/install/
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)