A year solo-building a real VPN: stack, abuse controls, and what broke in production.
Hi — I'm a full-stack engineer. For almost a year I've been solo-building FollowNet: iOS VPN client, Chrome extension, NestJS backend, VPN nodes, billing, monitoring. Not a landing-page MVP post — this is what the stack
Hi — I'm a full-stack engineer. For almost a year I've been solo-building FollowNet: iOS VPN client, Chrome extension, NestJS backend, VPN nodes, billing, monitoring.
Not a landing-page MVP post — this is what the stack looks like and what broke after shipping.
What it is
- iOS: IKEv2, WireGuard, AmneziaWG, Hysteria2, DNS profiles, auto-connect, Shortcuts, Smart Connect (pick protocol/server from network context)
- Chrome: browser proxy via SOCKS/gateway (not a system-wide VPN)
- Live in the App Store
Architecture (why modular monolith)
I'm one person. One NestJS API with domain modules beats microservices for me — fewer moving parts at 2am.
- Postgres (Prisma) = source of truth
- Redis = rate limits, bans, short-lived state, pub/sub
- FreeRADIUS = who gets into VPN and with which group (anon / free / premium)
- Nodes provisioned with Ansible (StrongSwan / WG / Amnezia / Hy2)
- Billing: StoreKit 2 + Apple Server Notifications on iOS; WayForPay on web
Protocol evolution
IKEv2 first (fastest path on iOS without a custom tunnel). Then WireGuard as the default dataplane. Then AmneziaWG + Hysteria2 for networks where plain UDP dies.
On iOS each protocol is a separate Network Extension target. Easier to debug early; cost is duplicated health-check / quota / reconnect logic. If I started today I'd put one Libbox-based core under them.
Smart Connect isn't "nearest ping wins" — geo/ASN rules + client success/fail telemetry so we don't start with a protocol that usually fails on that network.
Auth / access is a chain, not a flag
Three different credentials:
- App JWT → API
- RADIUS user/pass → VPN server (esp. IKEv2)
- Short-lived proxy JWT (~4h) → Chrome SOCKS gateway
Weekly free traffic resets Monday 00:00 UTC. Device slots: 2 free / 5 premium. Oldest lastSeenAt gets kicked when full.
Biggest pain: state drift. API says premium, RADIUS still free (or the reverse). Cron sync + Socket.IO pushes + sessionId so an old disconnect doesn't kill a fresh reconnect.
Backend abuse controls (honest scope)
Not "anti-DDoS for everything". Mostly API abuse:
- Helmet, CORS allowlist, DTO validation
- Redis sliding-window rate limits with escalating bans
- Disposable email blocklist
- Traffic exhausted → notify client → RADIUS CoA → (WG/AWG only) SSH peer remove as narrow fallback if CoA fails
- Apple JWS + WayForPay HMAC webhooks
Certificate pinning + dual API mirrors (.com / .net) on the client — some networks blackhole one host and the app looks "dead" even when VPN nodes are fine.
What actually broke in prod
- Connected but no internet — green tunnel, dead traffic. Fix: health monitors in extensions + quota gate at tunnel start.
- API config ≠ VPN access — RADIUS out of sync. Fix: CoA retries, CoA agent on node, sessionId race guards.
- LTE↔Wi-Fi handover — too-aggressive health checks flap; too soft = sit on a dead tunnel.
- Node deploy — Ansible helps; Amnezia installs can reboot forever; every new NAS must be in RADIUS clients or IKEv2 EAP times out.
- Chrome is a different product — no system VPN API; SOCKS + TLS gateway + Apple Sign-In relay via backend.
- Apple CONSUMPTION_REQUEST on refunds — answer with session usage, then on REFUND strip premium + update RADIUS.
What I'd do differently
- One shared session model across client / API / VPN from month one
- One Libbox core instead of multiple NE targets early
- Stabilize WG + accounting before Amnezia/Hy2
- More synthetic probes, fewer "wait for tickets"
- Write down "why this way" from the start
Three notes to past me
- API OK ≠ VPN works — design the chain to RADIUS/node, not just config download
- Connected ≠ internet — health + quota belong in the tunnel
- Don't multiply protocols/targets early — one stable path + accounting first
Happy to answer engineering questions. What would you redesign first on a solo VPN?
If you want to see the product: follow-net.com
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.
