DEV Community

Cover image for I built a free Bitly/TinyURL alternative and self-hosted it on a $6/mo VPS — here's the full stack
Tech Boy
Tech Boy

Posted on

I built a free Bitly/TinyURL alternative and self-hosted it on a $6/mo VPS — here's the full stack

I got tired of Bitly limiting me to 10 links/month for free and TinyURL having zero analytics. So I built meshalive.com — a free URL shortener with real-time click analytics, dynamic QR codes, custom slugs, and a REST API. Here's how it's built.

The stack

  • Backend: Go + Fiber — fast, minimal, compiles to a single binary
  • Database: PostgreSQL with sqlc for type-safe queries (no ORM)
  • Cache: Redis for sub-2ms redirect lookups — the short slug is looked up from Redis, DB is the fallback
  • Frontend: Next.js 15 (App Router) + TypeScript, zero UI framework, all inline styles
  • Infra: A single $6/mo VPS, Docker Compose, Nginx reverse proxy, Cloudflare in front
  • Auth: JWT access tokens (15min TTL) + HttpOnly refresh cookies (7 days), rotated on every refresh
  • Short domain: msha.live → every shortened link resolves through the Go API in <2ms

How the redirect works

Browser → Cloudflare → Nginx → Go API
slug lookup: Redis first → Postgres fallback
→ 301 to destination
→ async click event written to Postgres

The redirect path never touches Next.js — it goes directly to the Go API so there's no Node.js cold start penalty.

What's free (forever)

  • Shorten any URL instantly — no account needed
  • 100 saved links with a free account
  • Click analytics — geo, device, referrer
  • Dynamic QR codes (update destination after printing)
  • Custom slugs (msha.live/your-brand)
  • Full REST API on paid plans from $4/mo

Why self-hosted on one VPS instead of cloud functions?

Cost. A Lambda function + RDS + ElastiCache would run $40–80/mo for the same workload. The VPS runs everything for $6. Docker Compose means zero ops complexity — docker compose up -d and it's running.

What I learned

  • NEXT_PUBLIC_ env vars in Next.js are baked at build time, not runtime — cost me an hour
  • Docker bypasses UFW via iptables — remove port bindings from docker-compose.yml instead of using ufw deny
  • backdropFilter: blur() on a parent element breaks position: fixed for any child — the mobile nav drawer was invisible for this reason
  • Cloudflare caches 301 redirects in browsers permanently — test route changes in incognito

Try it: meshalive.com — shorten a URL, no account needed.

Happy to answer questions about the Go API, the sqlc setup, or the Nginx config.

Top comments (0)