R

2022—23 · SDE-1 · Full-stack + infra

Pulpy VPN.

A high-speed VPN, built from scratch and grown to 50K+ users.

Built at AppyHigh

StatusretiredStack16 tools
Pulpy VPN

Summary

A full VPN product — website, web app, Android, iOS, and a Chrome extension — all on one shared API, with on-demand server provisioning so idle servers didn't burn money.

  • Grew to 50,000+ users and improved analytics accuracy by ~90%
  • One shared API powering web, Android, iOS, and a Chrome extension
  • On-demand server provisioning so idle capacity didn't cost money
  • Owned backend, frontend, and DevOps while leading 2–3 interns

The brief

I built Pulpy VPN at AppyHigh as an SDE-1. The product existed, but it needed a rebuild: a faster service, a cleaner stack, and clients across every surface a user might want. I owned the backend, the frontend, and the infrastructure, and led 2–3 interns along the way.

VPNs are a commodity until they aren't. Users don't care about the protocol. They care that it connects fast, stays connected, and doesn't leak. The goal was to make the whole thing feel that way.

Users

50K+

Grown over the rebuild

Analytics accuracy

~90%

Improvement after the rework

Client surfaces

5

Web, app, Android, iOS, Chrome

The constraints

  • Five surfaces, one API. Website, web app, Android, iOS, and a Chrome extension all had to tell the same story. Keeping the API boundary tight was the single best decision on the project.
  • Servers are expensive when idle. Running a fleet of always-on VPN servers burns money. They had to come up on demand and drain when usage dropped.
  • Payments had to just work. Stripe on the web, store billing in the apps. RevenueCat bridged the two.

How it was built

The control plane

Every "VPN server" was really a recipe: an Ubuntu image, OpenVPN with a hardened set of cipher suites, Squid as an HTTP proxy for the Chrome-extension case, and PiHole baked in as an ad-blocker.

Terraform provisioned into AWS or DigitalOcean depending on regional pricing. A small Node service watched load and brought servers up or down as demand moved, so idle capacity didn't sit there costing money. Servers reported health back over a private channel; miss enough heartbeats and they were marked unhealthy and drained.

Data

MongoDB held the transactional data — users, subscriptions, referrals. Redis routed sessions: when a client connected, a lookup pointed it at the best healthy server. For analytics I kept it boring — raw events landed in their own collection, with nightly rollup jobs into daily, weekly, and monthly aggregates. That rework is where the ~90% accuracy improvement came from.

Clients

The website and web app were Next.js + TypeScript. The Android and iOS clients shared the same API. The Chrome extension was its own small world — it talked to the Squid proxy layer rather than OpenVPN, since you can't run a tunnel inside a browser.

What I'd do differently

  • Write the billing layer once, earlier. Stripe and in-app purchases have different mental models. I refactored that abstraction twice. Next time I'd spend a day designing the billing interface before writing any integration code.
  • Respect how much native eats. The mobile clients took more time than the rest of the stack combined. Cross-platform, or a thin wrapper around a shared core, would have bought back a lot of weeks.

Where it stands

The service has since been retired, but the architecture is something I'm still proud of — and I can still talk through its failure modes from memory.