Skip to main content

Plausible vs Umami vs Fathom: Privacy-First Web Analytics (2026)

·PkgPulse Team

TL;DR

Plausible is the lightweight, open-source analytics platform — no cookies, GDPR-compliant, simple dashboard, <1KB script, proxy support, self-hostable. Umami is the open-source web analytics alternative — cookie-free, customizable, team support, self-hostable, API-first. Fathom is the privacy-focused analytics SaaS — cookie-free, EU isolation, intelligent bot filtering, simple setup, built for businesses that need compliance. In 2026: Plausible for the simplest privacy analytics, Umami for open-source self-hosted analytics, Fathom for privacy-first SaaS with EU compliance.

Key Takeaways

  • Plausible: Open-source, <1KB script, no cookies, proxy support
  • Umami: Open-source, self-hosted, API-first, custom events
  • Fathom: SaaS, EU isolation, intelligent bot filtering, script <2KB
  • All three are cookie-free and GDPR-compliant by default
  • Plausible has the lightest script (~800 bytes)
  • Umami is the most customizable (open-source, full API)
  • Fathom has the best bot filtering and EU data isolation

Plausible

Plausible — lightweight open-source analytics:

Setup

<!-- Add to <head> — just one script tag: -->
<script
  defer
  data-domain="pkgpulse.com"
  src="https://plausible.io/js/script.js"
></script>

<!-- With custom events: -->
<script
  defer
  data-domain="pkgpulse.com"
  src="https://plausible.io/js/script.tagged-events.js"
></script>

<!-- Self-hosted: -->
<script
  defer
  data-domain="pkgpulse.com"
  src="https://analytics.pkgpulse.com/js/script.js"
></script>

Next.js integration

// app/layout.tsx
import Script from "next/script"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <Script
          defer
          data-domain="pkgpulse.com"
          src="https://plausible.io/js/script.js"
          strategy="afterInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

// Proxy through Next.js API route (avoid ad blockers):
// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: "/js/script.js",
        destination: "https://plausible.io/js/script.js",
      },
      {
        source: "/api/event",
        destination: "https://plausible.io/api/event",
      },
    ]
  },
}

Custom events and goals

// Track custom events:
declare global {
  interface Window {
    plausible: (event: string, options?: { props?: Record<string, string>; revenue?: { currency: string; amount: number } }) => void
  }
}

// Basic event:
window.plausible("Signup")

// Event with properties:
window.plausible("Package Compared", {
  props: {
    packages: "react vs vue",
    comparison_type: "downloads",
  },
})

// Revenue tracking:
window.plausible("Subscription", {
  revenue: { currency: "USD", amount: 29.99 },
  props: { plan: "pro" },
})

// CSS class-based events (no JS needed):
// <button class="plausible-event-name=Signup">Sign Up</button>
// <a class="plausible-event-name=Download+Report" href="/report.pdf">Download</a>

// React hook:
function usePlausible() {
  return (event: string, props?: Record<string, string>) => {
    if (typeof window !== "undefined" && window.plausible) {
      window.plausible(event, props ? { props } : undefined)
    }
  }
}

function CompareButton({ packages }: { packages: string[] }) {
  const plausible = usePlausible()

  return (
    <button onClick={() => plausible("Compare Clicked", {
      packages: packages.join(" vs "),
    })}>
      Compare
    </button>
  )
}

Stats API

// Plausible Stats API:
const PLAUSIBLE_API = "https://plausible.io/api/v1"
const headers = {
  Authorization: `Bearer ${process.env.PLAUSIBLE_API_KEY}`,
}

// Realtime visitors:
const realtime = await fetch(
  `${PLAUSIBLE_API}/stats/realtime/visitors?site_id=pkgpulse.com`,
  { headers }
)
console.log(`Current visitors: ${await realtime.text()}`)

// Aggregate stats:
const stats = await fetch(
  `${PLAUSIBLE_API}/stats/aggregate?site_id=pkgpulse.com&period=30d&metrics=visitors,pageviews,bounce_rate,visit_duration`,
  { headers }
)
const { results } = await stats.json()
console.log(`Visitors: ${results.visitors.value}`)
console.log(`Pageviews: ${results.pageviews.value}`)
console.log(`Bounce rate: ${results.bounce_rate.value}%`)

// Top pages:
const pages = await fetch(
  `${PLAUSIBLE_API}/stats/breakdown?site_id=pkgpulse.com&period=30d&property=event:page&limit=10`,
  { headers }
)
const { results: topPages } = await pages.json()
topPages.forEach((page: any) => {
  console.log(`${page.page}: ${page.visitors} visitors`)
})

// Top sources:
const sources = await fetch(
  `${PLAUSIBLE_API}/stats/breakdown?site_id=pkgpulse.com&period=30d&property=visit:source&limit=10`,
  { headers }
)

Umami

Umami — open-source web analytics:

Setup

<!-- Add tracking script: -->
<script
  defer
  src="https://analytics.pkgpulse.com/script.js"
  data-website-id="your-website-id"
></script>

<!-- With auto-track disabled: -->
<script
  defer
  src="https://analytics.pkgpulse.com/script.js"
  data-website-id="your-website-id"
  data-auto-track="false"
></script>

Next.js integration

// app/layout.tsx
import Script from "next/script"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <Script
          defer
          src="https://analytics.pkgpulse.com/script.js"
          data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
          strategy="afterInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

Custom events and tracking

// Umami tracking API:
declare global {
  interface Window {
    umami: {
      track: (event: string | Function, data?: Record<string, string | number>) => void
    }
  }
}

// Track page view manually:
window.umami.track()

// Track custom event:
window.umami.track("signup", { method: "email" })

// Track with properties:
window.umami.track("package-compared", {
  packages: "react vs vue",
  type: "downloads",
})

// Track using callback for dynamic data:
window.umami.track((props) => ({
  ...props,
  name: "custom-pageview",
  data: {
    path: window.location.pathname,
    theme: document.documentElement.dataset.theme || "light",
  },
}))

// CSS data attributes (no JS):
// <button data-umami-event="signup-click">Sign Up</button>
// <button data-umami-event="compare" data-umami-event-packages="react,vue">Compare</button>

// React hook:
function useUmami() {
  return {
    track: (event: string, data?: Record<string, string | number>) => {
      if (typeof window !== "undefined" && window.umami) {
        window.umami.track(event, data)
      }
    },
  }
}

function DownloadButton({ packageName }: { packageName: string }) {
  const umami = useUmami()

  return (
    <button
      onClick={() => umami.track("download-clicked", { package: packageName })}
      data-umami-event="download-clicked"
      data-umami-event-package={packageName}
    >
      Download
    </button>
  )
}

API access

// Umami API:
const UMAMI_API = "https://analytics.pkgpulse.com/api"
const headers = {
  Authorization: `Bearer ${process.env.UMAMI_API_KEY}`,
  "Content-Type": "application/json",
}

// Get website stats:
const stats = await fetch(
  `${UMAMI_API}/websites/${WEBSITE_ID}/stats?startAt=${startDate}&endAt=${endDate}`,
  { headers }
)
const data = await stats.json()
console.log(`Pageviews: ${data.pageviews.value}`)
console.log(`Visitors: ${data.visitors.value}`)
console.log(`Bounces: ${data.bounces.value}`)

// Get pageviews over time:
const pageviews = await fetch(
  `${UMAMI_API}/websites/${WEBSITE_ID}/pageviews?startAt=${startDate}&endAt=${endDate}&unit=day`,
  { headers }
)
const { pageviews: daily } = await pageviews.json()
daily.forEach((day: any) => {
  console.log(`${day.date}: ${day.y} views`)
})

// Get top pages:
const pages = await fetch(
  `${UMAMI_API}/websites/${WEBSITE_ID}/metrics?startAt=${startDate}&endAt=${endDate}&type=url`,
  { headers }
)
const topPages = await pages.json()
topPages.forEach((page: any) => {
  console.log(`${page.x}: ${page.y} views`)
})

// Get events:
const events = await fetch(
  `${UMAMI_API}/websites/${WEBSITE_ID}/events?startAt=${startDate}&endAt=${endDate}`,
  { headers }
)

// Send event via API (server-side):
await fetch(`${UMAMI_API}/send`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    payload: {
      website: WEBSITE_ID,
      name: "api-package-lookup",
      data: { package: "react" },
      hostname: "api.pkgpulse.com",
      language: "en-US",
      url: "/api/packages/react",
    },
    type: "event",
  }),
})

Self-hosting

# Docker Compose:
# docker-compose.yml
version: "3"
services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://umami:password@db:5432/umami
      DATABASE_TYPE: postgresql
      APP_SECRET: your-secret-key
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: password
    volumes:
      - umami-data:/var/lib/postgresql/data

volumes:
  umami-data:

Fathom

Fathom — privacy-focused analytics SaaS:

Setup

<!-- Fathom script: -->
<script
  src="https://cdn.usefathom.com/script.js"
  data-site="ABCDEFGH"
  defer
></script>

<!-- With custom domain (avoid ad blockers): -->
<script
  src="https://pkgpulse-analytics.pkgpulse.com/script.js"
  data-site="ABCDEFGH"
  defer
></script>

<!-- SPA support: -->
<script
  src="https://cdn.usefathom.com/script.js"
  data-site="ABCDEFGH"
  data-spa="auto"
  defer
></script>

Next.js integration

// Using fathom-client package:
import * as Fathom from "fathom-client"
import { useEffect } from "react"
import { usePathname, useSearchParams } from "next/navigation"

// app/components/FathomAnalytics.tsx
export function FathomAnalytics() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    Fathom.load("ABCDEFGH", {
      includedDomains: ["pkgpulse.com", "www.pkgpulse.com"],
      url: "https://pkgpulse-analytics.pkgpulse.com/script.js",  // Custom domain
    })
  }, [])

  // Track page views on route change:
  useEffect(() => {
    Fathom.trackPageview()
  }, [pathname, searchParams])

  return null
}

// app/layout.tsx
import { FathomAnalytics } from "./components/FathomAnalytics"
import { Suspense } from "react"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Suspense fallback={null}>
          <FathomAnalytics />
        </Suspense>
        {children}
      </body>
    </html>
  )
}

Custom events and goals

import * as Fathom from "fathom-client"

// Track event (goal):
Fathom.trackEvent("Signup")

// Track with monetary value (cents):
Fathom.trackEvent("Subscription", { _value: 2999 })  // $29.99

// Track comparison:
Fathom.trackEvent("Package Compared")

// Track download:
Fathom.trackEvent("Report Downloaded")

// React hook:
function useFathom() {
  return {
    trackEvent: (name: string, value?: number) => {
      Fathom.trackEvent(name, value ? { _value: value } : undefined)
    },
    trackPageview: () => Fathom.trackPageview(),
  }
}

function CompareButton({ packages }: { packages: string[] }) {
  const fathom = useFathom()

  return (
    <button onClick={() => {
      fathom.trackEvent("Compare Clicked")
      // Navigate to comparison
    }}>
      Compare {packages.length} Packages
    </button>
  )
}

function PricingCard({ plan, price }: { plan: string; price: number }) {
  const fathom = useFathom()

  return (
    <button onClick={() => {
      fathom.trackEvent("Plan Selected", price * 100)  // Value in cents
    }}>
      Subscribe to {plan} — ${price}/mo
    </button>
  )
}

API access

// Fathom API:
const FATHOM_API = "https://api.usefathom.com/v1"
const headers = {
  Authorization: `Bearer ${process.env.FATHOM_API_KEY}`,
}

// Get site details:
const sites = await fetch(`${FATHOM_API}/sites`, { headers })
const { data: siteList } = await sites.json()
siteList.forEach((site: any) => {
  console.log(`${site.name}: ${site.id}`)
})

// Get aggregated stats:
const aggregation = await fetch(
  `${FATHOM_API}/aggregations?entity=pageview&entity_id=${SITE_ID}&aggregates=visits,uniques,pageviews,avg_duration,bounce_rate&date_from=2026-02-01&date_to=2026-03-01`,
  { headers }
)
const stats = await aggregation.json()
console.log(`Visits: ${stats[0].visits}`)
console.log(`Uniques: ${stats[0].uniques}`)
console.log(`Pageviews: ${stats[0].pageviews}`)
console.log(`Avg duration: ${stats[0].avg_duration}s`)

// Get top pages:
const pages = await fetch(
  `${FATHOM_API}/aggregations?entity=pageview&entity_id=${SITE_ID}&aggregates=pageviews&field_grouping=pathname&date_from=2026-02-01&date_to=2026-03-01&sort_by=pageviews:desc&limit=10`,
  { headers }
)

// Get events (goals):
const events = await fetch(`${FATHOM_API}/sites/${SITE_ID}/events`, { headers })
const { data: eventList } = await events.json()
eventList.forEach((event: any) => {
  console.log(`${event.name}: ${event.id}`)
})

// Get event completions:
const completions = await fetch(
  `${FATHOM_API}/aggregations?entity=event&entity_id=${EVENT_ID}&aggregates=completions,value&date_from=2026-02-01&date_to=2026-03-01&date_grouping=day`,
  { headers }
)

Feature Comparison

FeaturePlausibleUmamiFathom
Open-source
Self-hosted
Cookie-free
GDPR compliant
Script size~800 bytes~2KB~1.5KB
Custom events✅ (goals)
Event properties❌ (value only)
Revenue tracking
Bot filtering✅ (basic)✅ (basic)✅ (intelligent)
EU data isolation✅ (EU servers)✅ (self-host)✅ (EU isolation)
Custom domains✅ (proxy)✅ (self-host)✅ (built-in)
API access
Team support
Realtime
SPA support
Import GA data
Free tierSelf-hostSelf-host
PricingFrom $9/monthFree (self-host) / cloudFrom $15/month

When to Use Each

Use Plausible if:

  • Want the lightest analytics script (~800 bytes)
  • Need open-source with option to self-host or use cloud
  • Prefer the simplest, cleanest dashboard
  • Want proxy support to avoid ad blockers

Use Umami if:

  • Want fully open-source analytics you self-host
  • Need custom event tracking with properties
  • Want API-first analytics for building custom dashboards
  • Prefer maximum control over your analytics infrastructure

Use Fathom if:

  • Want privacy-first SaaS with zero maintenance
  • Need intelligent bot filtering and EU data isolation
  • Building a business that needs compliance guarantees
  • Want built-in custom domains without proxy setup

Methodology

Feature comparison based on Plausible, Umami, and Fathom platforms and pricing as of March 2026.

Compare analytics and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.