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
| Feature | Plausible | Umami | Fathom |
|---|---|---|---|
| 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 tier | Self-host | Self-host | ❌ |
| Pricing | From $9/month | Free (self-host) / cloud | From $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
GDPR Compliance Without Cookie Consent Banners
The primary business value of privacy-first analytics in 2026 is operational: by collecting no personal data and placing no cookies, these platforms eliminate the need for cookie consent banners in most EU jurisdictions under GDPR's legitimate interest basis for analytics. This removes both the development cost of implementing cookie consent infrastructure and the conversion rate impact of requiring users to interact with cookie consent dialogs before accessing content. Plausible, Umami, and Fathom all use a fingerprinting approach that does not persist across days or identify individual users — instead of storing a persistent user ID in a cookie, they hash a combination of IP address, user agent, and a daily rotating salt to create a temporary session identifier. This approach satisfies most interpretations of GDPR's privacy requirements without a consent requirement, though organizations in regulated industries should verify this assessment with their own legal counsel given that EU data protection authority interpretations vary by country.
Self-Hosting Plausible and Umami in Production
For teams self-hosting analytics, the operational requirements differ between Plausible and Umami. Plausible self-hosted requires PostgreSQL for user and site configuration data and Clickhouse as the event storage backend — Clickhouse is a columnar analytical database optimized for the kind of time-series aggregation queries that analytics requires, and it handles hundreds of millions of events without the performance degradation that would occur storing events in PostgreSQL. The Plausible Docker Compose configuration sets up both databases, but teams should plan for Clickhouse disk usage (typically 100-500GB for millions of monthly pageviews) when sizing their infrastructure. Umami is simpler to self-host because it stores all data in PostgreSQL (or MySQL), which most teams already operate. The tradeoff is that Umami's query performance degrades at very high event volumes compared to Plausible's Clickhouse backend, though for sites under 50 million monthly pageviews, PostgreSQL performs adequately with proper indexing.
Custom Event Tracking and Analytics Architecture
The custom event tracking capabilities of each platform determine how deeply you can instrument your application's user behavior. Plausible and Umami both support event properties (custom dimensions attached to events), which is essential for answering business questions like "which package categories drive the most comparisons?" or "which pricing plan pages have the highest conversion rate?" Fathom tracks events as goals but supports only a single monetary value dimension per event, not arbitrary string properties — this limits the analytical depth possible with Fathom for complex behavior tracking. For server-side event tracking (important for events that happen in Next.js Server Actions or API routes where there is no browser context), all three platforms accept events via their HTTP collection API, so backend-triggered events like API requests, background job completions, or payment events can be tracked alongside pageviews. Umami's server-side tracking API is the most flexible for this use case, accepting arbitrary event data without the user-agent and screen-size fields that browser tracking automatically provides.
Proxy and Ad-Blocker Bypassing
Ad blockers and browser privacy extensions block analytics scripts from known domains, which can cause 20-40% of pageviews to go untracked depending on your audience's privacy tooling adoption. All three platforms provide solutions, but the implementation differs. Plausible's proxy approach routes the analytics script and event collection API through your own domain (using Next.js rewrites, Nginx, or a CDN worker), so the requests appear to come from your own domain rather than plausible.io. This bypasses most ad blockers because they block known tracker domains but cannot block your own domain. Fathom provides custom domains as a first-class feature — you can configure a subdomain like analytics.yourdomain.com that Fathom serves from, with no proxy configuration required on your end. Umami self-hosting inherently solves this problem because the script is served from your own domain, and you can alias the script URL to make it less identifiable as analytics traffic.
Data Portability and Migration Between Platforms
Long-term ownership of your analytics data matters when evaluating analytics platforms. Plausible provides a data export feature that downloads your event data as CSV, covering the full event history stored in Clickhouse. The exported data can be re-imported into another Plausible instance or processed externally. Umami's PostgreSQL storage makes data access particularly flexible — because it's a standard relational database, you can query the raw events table directly using SQL, export to any format, or integrate with BI tools like Metabase or Grafana for custom reporting beyond the Umami dashboard. Fathom's data portability is limited compared to self-hosted alternatives — data export is available through the API but not as a bulk historical export in the current free tier. For teams evaluating long-term platform commitment, Umami's self-hosted PostgreSQL model provides the strongest data independence, while Fathom's SaaS model requires trusting Fathom's continued operation and export capabilities.
Team Access and Multi-Site Management
For agencies, consultancies, or SaaS companies managing analytics across many client or product sites, multi-site management and access control are critical platform selection criteria. Plausible supports multiple sites under a single account, with each site having its own dashboard, and supports inviting team members with viewer or admin roles per site — useful for giving clients read-only access to their own analytics without exposing other sites. Fathom's team access model similarly supports multiple sites and team member invitations with configurable permissions, and its site-specific API keys allow different team members or integrations to access only the data they need. Umami's self-hosted model provides full flexibility here: you manage users and sites in the database directly, and the application UI supports creating multiple sites and assigning users to specific sites. For larger teams using Umami Cloud, site-based access control is managed through the dashboard. When managing analytics for external clients, Fathom's white-labeling for client reporting dashboards (available on higher plans) or Umami's self-hosted approach with a custom domain are both viable options for presenting analytics data without exposing the underlying platform vendor to clients.
Methodology
Feature comparison based on Plausible, Umami, and Fathom platforms and pricing as of March 2026.
Compare analytics and developer tooling on PkgPulse →
See also: AVA vs Jest and PostHog vs Mixpanel vs Amplitude, Hotjar vs FullStory vs Microsoft Clarity.