Migrating Off Google Analytics: Umami vs Plausible vs Fathom
The wake-up call I didn't ask for Last week the TanStack folks reported what appears to be a compromise affecting some of their NPM packages (the details are still being sorted out in issue #7383 — read it yourself before drawing conclusions). I won't rehash the postmortem here. What I want to talk about is the gut-punch feeling I had reading it. I run npm install every day. I've barely thought about which third-party scripts are loading in production. And one of the worst offenders sitting in nearly every site I've ever shipped? Analytics. So this post is about something I've been chewing on for months but finally moved on: ripping Google Analytics out of three side projects and picking a privacy-focused alternative. Specifically, I'll compare Umami, Plausible, and Fathom — the three I actually evaluated — and walk through the migration steps that worked for me. A few honest reasons, none of them ideological: Script weight. GA4's gtag.js is heavy. The privacy-focused tools are typically 1–2 KB. Cookie banners. No cookies = no consent banner in most jurisdictions. Fewer modals = fewer bounces. Vendor trust. After watching a supply chain story unfold in real time, having fewer third-party scripts feels less reckless. Self-hosting option. If I can run it on my own infra, I control the script. If you genuinely need Google's audience features (remarketing, conversion linking to Google Ads), this post probably isn't for you. Stay where you are. Open source (AGPL), GDPR/CCPA compliant, cloud or self-hosted. The script is small — the docs claim under 1 KB. Written in Elixir. Cloud plans are subscription-based. Privacy-focused, cloud-only since they pivoted from the original open source v1 ("Fathom Lite," archived) to a commercial closed-source product. I evaluated the commercial product. Open source (MIT), self-hosted by default with a hosted cloud option on umami.is. Built on Next.js, runs on PostgreSQL or MySQL. Free if you host it yourself. Easy enough that I had it running in an evening. I'll keep this honest — I ran all three on the same site for two weeks before deciding. Feature Plausible Fathom Umami Open source Yes (AGPL) No (closed) Yes (MIT) Self-host Yes No Yes (primary path) Cookies No No No GDPR Yes Yes Yes Cloud option Paid Paid Free tier + paid Script size ~1 KB ~2 KB ~2 KB Funnels / goals Yes Yes Yes (basic) The sizes above match what I observed in the network tab, but check each vendor's docs before quoting them anywhere serious. Replacing GA is mostly about swapping a script tag. Here's the before: window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-XXXXXX'); // sends pageview + sets cookies And the replacements: That's it. No dataLayer. No consent banner gate. The script loads once, sends a single beacon per pageview, and stops bothering you. The thing I almost forgot when migrating: GA's gtag('event', ...) calls. Here's how I rewrote them for Umami (the APIs are similar across the three, but each has its own conventions): // Before (GA4) gtag('event', 'signup_completed', { plan: 'pro', source: 'pricing_page' }); // After (Umami) // `umami` is attached to window by the loader script window.umami?.track('signup_completed', { plan: 'pro', source: 'pricing_page' }); Plausible uses window.plausible('signup_completed', { props: { plan: 'pro' } }). Fathom uses fathom.trackEvent('signup_completed'). Don't do a global find-and-replace — the property conventions differ enough that you'll want to read each vendor's docs first. This is the part that sold me. Here's the docker-compose.yml running on the VPS for one of my side projects: services: umami: image: ghcr.io/umami-software/umami:postgresql-latest ports: - "3000:3000" environment: DATABASE_URL: postgresql://umami:umami@db:5432/umami DATABASE_TYPE: postgresql APP_SECRET: change-me-to-a-real-secret # rotate this depends_on: db: condition: service_healthy db: image: postgres:15-alpine environment: POSTGRES_DB: umami POSTGRES_USER: umami POSTGRES_PASSWORD: umami volumes: - umami-db:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U umami"] volumes: umami-db: Run it behind Caddy or Nginx, point a subdomain at it, drop the script tag into your site. You own the data. Nothing leaves your server. The dashboard is genuinely pleasant — the Next.js UI loads fast and shows the things I actually look at. No magic, just mechanical: Inventory your GA calls. Grep your codebase for gtag(, dataLayer, and any analytics wrapper functions. Write them down. Pick your destination. Zero ongoing cost and own your data → self-hosted Umami. Don't want to run Postgres → Plausible Cloud. Want the most polished commercial dashboard → Fathom. Run them in parallel for a week. Drop the new script alongside GA. Compare daily pageview counts. You'll see drift — the privacy-focused tools usually report fewer visits because they don't fingerprint, and that's kind of the point. Rewrite custom events. Map each gtag('event', ...) to the new API. Wrap them in a helper so you can switch again later without grepping. Remove the GA script and the cookie banner. This is the satisfying part. Honestly? Here's how I'd choose: Side projects, solo devs: Self-hosted Umami. Free, simple, MIT-licensed. Small business, no ops appetite: Plausible Cloud. Easiest onboarding, still open source if you ever want to migrate off. Polished dashboards for clients: Fathom. The UX feels the most "finished" of the three. I'm not saying Google Analytics is bad — it's free, it's powerful, and it's still the right answer if you live inside their ad ecosystem. But for the rest of us, three lines of script and a Postgres container will get you 90% of what you actually look at, with one less third-party domain in your Content-Security-Policy. The TanStack situation reminded me that every script tag is a trust decision. Make fewer trust decisions.
