\n\n\n"}},{"@type":"Question","name":"How to migrate from Google Analytics?","acceptedAnswer":{"@type":"Answer","text":"1. Add the Plausible script alongside GA\n2. Compare data for 2-4 weeks\n3. Define goals matching your GA events\n4. Remove the GA script after verification\n\nPlausible also offers historical data import from Google Analytics."}},{"@type":"Question","name":"Can I use Plausible to track mobile apps?","acceptedAnswer":{"@type":"Answer","text":"This tool is designed primarily for websites. For mobile apps, something oriented around product events makes more sense, PostHog for instance. Events can be sent from an application through the API, but the interface itself is built around web traffic."}},{"@type":"Question","name":"How does the free edition differ from the cloud?","acceptedAnswer":{"@type":"Answer","text":"The community edition is not equivalent to the paid service. It lacks funnels, Google Analytics data import, and team single sign on. This is the most common misconception about the product."}}]}
We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide19 min read

Plausible - privacy-first analytics

Plausible is lightweight cookieless analytics you can self host. Pricing, what the free edition lacks, and a comparison with Google Analytics.

Plausible - privacy-first analytics

What is Plausible?

Plausible Analytics is a lightweight, open-source analytics platform created as an alternative to Google Analytics with user privacy in mind. Founded in 2019 in Estonia by Uku Taht and Marko Saric, Plausible grew out of the need for an analytics tool that does not require cookies, GDPR consent banners, and does not collect personal data.

Unlike traditional analytics tools that track users across the web using third-party cookies, Plausible uses a unique approach based on hashing IP addresses combined with User Agent. This means every visit is anonymous, and Plausible does not build user profiles or sell data to advertisers.

History and philosophy of Plausible

Plausible was created as a response to growing concerns about internet privacy and increasingly complex regulations like GDPR (Europe), CCPA (California), and LGPD (Brazil). The creators noticed that most website owners only need basic metrics -- how many people visit the site, where they come from, and which pages are popular.

Google Analytics offers thousands of metrics and dimensions, but most of them are not needed by regular content creators or small businesses. Yet it requires accepting a complicated privacy policy, installing cookie banners, and potentially exposes you to GDPR fines of up to 4% of annual revenue.

Plausible follows the "less is more" philosophy -- a simple dashboard with the most important information, without unnecessary noise and without compromising on privacy.

Why Plausible?

1. No cookies = no GDPR banners

Plausible does not use cookies or any form of persistent storage. You do not need to display annoying banners asking for cookie consent. Your site loads faster and looks more professional without those pop-ups.

2. Extremely lightweight script

The Plausible script weighs less than 1KB (gzipped), while Google Analytics is over 45KB. This means:

  • Faster page loading
  • Better Core Web Vitals score
  • Lower bandwidth usage for visitors
  • Smaller carbon footprint for your site

3. Full regulatory compliance

Plausible is fully compliant with:

  • GDPR (Europe)
  • CCPA (California)
  • PECR (UK)
  • LGPD (Brazil)
  • PIPEDA (Canada)

Data is stored on servers in the European Union, which is a requirement for many European companies and institutions.

4. Open source and self-hosting

Plausible is fully open source (AGPL license). You can:

  • See exactly how the code works
  • Run your own instance on your servers
  • Have 100% control over data
  • Never worry about vendor lock-in

5. Simple, readable dashboard

Instead of hundreds of reports and dimensions, you get one elegant dashboard with the most important information:

  • Unique visitors
  • Pageviews
  • Bounce rate
  • Time on page
  • Traffic sources
  • Popular pages
  • Geographic locations
  • Devices and browsers

6. Ethical approach to business

Plausible is a bootstrapped company without external investors. This means:

  • No pressure to monetize user data
  • Transparent business model (you pay for the service, you are not the product)
  • Long-term stability without the risk of corporate acquisition

Plausible vs Google Analytics

FeaturePlausibleGoogle Analytics 4
CookiesNoYes (first-party)
GDPR bannerNot requiredRequired
Script size<1KB45KB+
Personal dataDoes not collectCollects
User profilingNoYes
Sharing with GoogleNoYes (by default)
Open sourceYes (AGPL)No
Self-hostingYesNo
PriceFrom $9/mo"Free"*
ComplexitySimpleVery complex
Data retentionIndefinite2-14 months
Real-timeYesLimited
Data locationEU (Germany)USA (Google Cloud)

*Google Analytics is "free", but you pay with your users' data, which Google uses for advertising.

Plausible vs other alternatives

FeaturePlausibleFathomSimple AnalyticsMatomo
Open sourceYesNoNoYes
Self-hostYesNoNoYes
Price (start)$9/mo$14/mo$9/moFree
CookiesNoNoNoOptional
JS size<1KB2KB3KB22KB+
EU hostingYesYesYesOptional
Real-timeYesYesYesYes
Custom eventsYesYesYesYes
FunnelsYesYesYesYes
APIYesYesYesYes

Installing Plausible

Basic installation (HTML)

Add the script to the <head> section of your page:

Code
HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My website</title>

    <!-- Plausible Analytics -->
    <script
        defer
        data-domain="yourdomain.com"
        src="https://plausible.io/js/script.js">
    </script>
</head>
<body>
    <!-- Page content -->
</body>
</html>

Next.js App Router

TSapp/layout.tsx
TypeScript
// app/layout.tsx
import Script from 'next/script'

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

Next.js Pages Router

TSpages/_app.tsx
TypeScript
// pages/_app.tsx
import Script from 'next/script'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Script
        defer
        data-domain="yourdomain.com"
        src="https://plausible.io/js/script.js"
        strategy="afterInteractive"
      />
      <Component {...pageProps} />
    </>
  )
}

Remix

In Remix the tag goes into app/root.tsx, because that file renders the whole document including the head. One caveat concerns the name: development of this line moved into React Router 7, so the example below describes version 2, and in a new project you make the same edit in React Router's root file.

TSapp/root.tsx
TypeScript
// app/root.tsx
import { Links, Meta, Outlet, Scripts } from '@remix-run/react'

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
        <script
          defer
          data-domain="yourdomain.com"
          src="https://plausible.io/js/script.js"
        />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  )
}

Astro

Code
ASTRO
---
// src/layouts/Layout.astro
---
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>{title}</title>
    <script
        defer
        data-domain="yourdomain.com"
        src="https://plausible.io/js/script.js">
    </script>
</head>
<body>
    <slot />
</body>
</html>

Gatsby

JSgatsby-ssr.js
JavaScript
// gatsby-ssr.js
import React from 'react'

export const onRenderBody = ({ setHeadComponents }) => {
  setHeadComponents([
    <script
      key="plausible"
      defer
      data-domain="yourdomain.com"
      src="https://plausible.io/js/script.js"
    />
  ])
}

Nuxt.js 3

TSnuxt.config.ts
TypeScript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          defer: true,
          'data-domain': 'yourdomain.com',
          src: 'https://plausible.io/js/script.js'
        }
      ]
    }
  }
})

SvelteKit

src/app.html
HTML
<!-- src/app.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    %sveltekit.head%
    <script
        defer
        data-domain="yourdomain.com"
        src="https://plausible.io/js/script.js">
    </script>
</head>
<body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
</body>
</html>

Custom events

Tracking events with JavaScript

Code
TypeScript
declare global {
  interface Window {
    plausible: (
      event: string,
      options?: {
        props?: Record<string, string | number | boolean>
        callback?: () => void
        revenue?: { currency: string; amount: number }
      }
    ) => void
  }
}

function trackSignup() {
  window.plausible('Signup')
}

function trackPurchase(plan: string, price: number) {
  window.plausible('Purchase', {
    props: {
      plan,
      price: price.toString()
    }
  })
}

function trackSale(amount: number, currency: string = 'USD') {
  window.plausible('Sale', {
    revenue: { currency, amount }
  })
}

function trackDownload(fileName: string) {
  window.plausible('Download', {
    props: { file: fileName },
    callback: () => {
      console.log('Event tracked successfully')
    }
  })
}

React hook

TShooks/usePlausible.ts
TypeScript
// hooks/usePlausible.ts
import { useCallback } from 'react'

type PlausibleEvent = {
  name: string
  props?: Record<string, string | number | boolean>
  revenue?: { currency: string; amount: number }
}

export function usePlausible() {
  const trackEvent = useCallback(({ name, props, revenue }: PlausibleEvent) => {
    if (typeof window !== 'undefined' && window.plausible) {
      window.plausible(name, { props, revenue })
    }
  }, [])

  const trackPageview = useCallback((url?: string) => {
    if (typeof window !== 'undefined' && window.plausible) {
      window.plausible('pageview', {
        props: url ? { url } : undefined
      })
    }
  }, [])

  return { trackEvent, trackPageview }
}

function PricingCard({ plan, price }: { plan: string; price: number }) {
  const { trackEvent } = usePlausible()

  const handlePurchase = () => {
    trackEvent({
      name: 'Purchase',
      props: { plan },
      revenue: { currency: 'USD', amount: price }
    })
  }

  return (
    <button onClick={handlePurchase}>
      Buy {plan} plan for ${price}
    </button>
  )
}

Automatic click tracking

Code
HTML
<!-- Add the class plausible-event-name=NAME to the element -->
<a
  href="/pricing"
  class="plausible-event-name=CTA+Click"
>
  View pricing
</a>

<!-- With properties -->
<button
  class="plausible-event-name=Button+Click plausible-event-variant=primary"
>
  Sign up
</button>

<!-- For forms -->
<form
  action="/submit"
  class="plausible-event-name=Form+Submit"
>
  <input type="email" name="email" required />
  <button type="submit">Submit</button>
</form>

Extended script options

Code
HTML
<!-- Automatic outbound link tracking -->
<script
  defer
  data-domain="yourdomain.com"
  src="https://plausible.io/js/script.outbound-links.js">
</script>

<!-- File download tracking -->
<script
  defer
  data-domain="yourdomain.com"
  src="https://plausible.io/js/script.file-downloads.js">
</script>

<!-- All extensions -->
<script
  defer
  data-domain="yourdomain.com"
  src="https://plausible.io/js/script.tagged-events.outbound-links.file-downloads.js">
</script>

<!-- Hash-based routing (SPA) -->
<script
  defer
  data-domain="yourdomain.com"
  src="https://plausible.io/js/script.hash.js">
</script>

<!-- Manual pageview tracking for SPA -->
<script
  defer
  data-domain="yourdomain.com"
  src="https://plausible.io/js/script.manual.js">
</script>

Goals & conversions

Defining goals in the dashboard

In the Plausible dashboard you can define different types of goals:

1. Pageview goals

  • /pricing - visits to the pricing page
  • /thank-you - thank you page after purchase
  • /signup/complete - registration completion

2. Custom events

  • Signup - user registration
  • Purchase - purchase
  • Download - file download
  • Newsletter - newsletter subscription

3. Funnels (conversion funnels)

Code
TEXT
Funnel: Purchase process
1. /products →
2. /cart →
3. /checkout →
4. /thank-you

Conversion: 2.3%

Revenue tracking

Code
TypeScript
function trackPurchase(orderId: string, amount: number) {
  window.plausible('Purchase', {
    revenue: {
      currency: 'USD',
      amount
    },
    props: {
      order_id: orderId
    }
  })
}

async function completeCheckout(cart: CartItem[]) {
  const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0)

  const response = await fetch('/api/checkout', {
    method: 'POST',
    body: JSON.stringify(cart)
  })

  if (response.ok) {
    const { orderId } = await response.json()

    window.plausible('Purchase', {
      revenue: { currency: 'USD', amount: total },
      props: {
        order_id: orderId,
        items_count: cart.length.toString()
      }
    })
  }
}

Filtering and segmentation

Available filters in the dashboard

Plausible lets you filter data by:

Traffic sources:

  • Source (Google, Facebook, Twitter, etc.)
  • Referrer (specific referring page)
  • UTM parameters (campaign, source, medium, term, content)

Technology:

  • Browser (Chrome, Firefox, Safari, Edge)
  • OS (Windows, macOS, iOS, Android, Linux)
  • Device type (Desktop, Mobile, Tablet)
  • Country / Region / City

Pages:

  • Entry page
  • Exit page
  • Page (visited page)

UTM tracking

Code
HTML
<!-- Link with UTM parameters -->
<a href="https://yourdomain.com?utm_source=newsletter&utm_medium=email&utm_campaign=summer_sale">
  Check out the promotion
</a>
Code
TypeScript
function createUtmLink(
  baseUrl: string,
  params: {
    source: string
    medium: string
    campaign: string
    term?: string
    content?: string
  }
) {
  const url = new URL(baseUrl)
  url.searchParams.set('utm_source', params.source)
  url.searchParams.set('utm_medium', params.medium)
  url.searchParams.set('utm_campaign', params.campaign)
  if (params.term) url.searchParams.set('utm_term', params.term)
  if (params.content) url.searchParams.set('utm_content', params.content)
  return url.toString()
}

const newsletterLink = createUtmLink('https://mystore.com/promo', {
  source: 'newsletter',
  medium: 'email',
  campaign: 'black_friday_2024'
})

Plausible API

Access to the stats API is tied to the Business plan and above rather than available at every tier. A limit of six hundred requests an hour applies, which a dashboard querying the API on every page refresh exhausts sooner than it looks. A sensible arrangement caches responses on your side and polls at fixed intervals rather than on user demand.

Stats API

Code
TypeScript
const PLAUSIBLE_API_KEY = process.env.PLAUSIBLE_API_KEY
const SITE_ID = 'yourdomain.com'

interface PlausibleStats {
  results: {
    visitors: { value: number }
    pageviews: { value: number }
    bounce_rate: { value: number }
    visit_duration: { value: number }
  }
}

async function getStats(period: string = '30d'): Promise<PlausibleStats> {
  const response = await fetch(
    `https://plausible.io/api/v1/stats/aggregate?` +
    `site_id=${SITE_ID}&period=${period}&` +
    `metrics=visitors,pageviews,bounce_rate,visit_duration`,
    {
      headers: {
        Authorization: `Bearer ${PLAUSIBLE_API_KEY}`
      }
    }
  )

  return response.json()
}

interface TopPages {
  results: Array<{
    page: string
    visitors: number
  }>
}

async function getTopPages(limit: number = 10): Promise<TopPages> {
  const response = await fetch(
    `https://plausible.io/api/v1/stats/breakdown?` +
    `site_id=${SITE_ID}&period=30d&property=event:page&limit=${limit}`,
    {
      headers: {
        Authorization: `Bearer ${PLAUSIBLE_API_KEY}`
      }
    }
  )

  return response.json()
}

Real-time API

Code
TypeScript
async function getCurrentVisitors(): Promise<number> {
  const response = await fetch(
    `https://plausible.io/api/v1/stats/realtime/visitors?site_id=${SITE_ID}`,
    {
      headers: {
        Authorization: `Bearer ${PLAUSIBLE_API_KEY}`
      }
    }
  )

  return response.json()
}

function LiveVisitors() {
  const [visitors, setVisitors] = useState<number>(0)

  useEffect(() => {
    const fetchVisitors = async () => {
      const count = await getCurrentVisitors()
      setVisitors(count)
    }

    fetchVisitors()
    const interval = setInterval(fetchVisitors, 30000)

    return () => clearInterval(interval)
  }, [])

  return (
    <div className="flex items-center gap-2">
      <span className="relative flex h-3 w-3">
        <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
        <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
      </span>
      <span>{visitors} active users</span>
    </div>
  )
}

Events API

Code
TypeScript
async function trackServerEvent(
  eventName: string,
  props?: Record<string, string>,
  userAgent?: string,
  ip?: string
) {
  await fetch('https://plausible.io/api/event', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent': userAgent || 'Node.js',
      'X-Forwarded-For': ip || ''
    },
    body: JSON.stringify({
      name: eventName,
      url: `https://${SITE_ID}`,
      domain: SITE_ID,
      props
    })
  })
}

export async function POST(request: Request) {
  const { action, data } = await request.json()

  await trackServerEvent(
    'API Action',
    { action, status: 'success' },
    request.headers.get('user-agent') || undefined,
    request.headers.get('x-forwarded-for') || undefined
  )

  return Response.json({ success: true })
}

Self-hosting Plausible

Docker Compose setup

docker-compose.yml
YAML
# docker-compose.yml
version: "3.8"

services:
  plausible:
    image: plausible/analytics:v2.0
    restart: always
    command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    depends_on:
      - plausible_db
      - plausible_events_db
    ports:
      - 8000:8000
    env_file:
      - plausible-conf.env

  plausible_db:
    image: postgres:14-alpine
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=postgres

  plausible_events_db:
    image: clickhouse/clickhouse-server:23.3.7.5-alpine
    restart: always
    volumes:
      - event-data:/var/lib/clickhouse
      - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
      - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144

volumes:
  db-data:
  event-data:

Environment configuration

plausible-conf.env
ENV
# plausible-conf.env

# Required settings
BASE_URL=https://analytics.yourdomain.com
SECRET_KEY_BASE=random-64-character-string-generate-with-openssl

# Database
DATABASE_URL=postgres://postgres:postgres@plausible_db:5432/plausible_db
CLICKHOUSE_DATABASE_URL=http://plausible_events_db:8123/plausible_events_db

# Email (optional, for notifications)
MAILER_EMAIL=plausible@yourdomain.com
SMTP_HOST_ADDR=smtp.yourdomain.com
SMTP_HOST_PORT=587
SMTP_USER_NAME=plausible@yourdomain.com
SMTP_USER_PWD=smtp-password
SMTP_HOST_SSL_ENABLED=false
SMTP_RETRIES=2

# Optional
DISABLE_REGISTRATION=true
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

Running the instance

Code
Bash
# Download configuration files
git clone https://github.com/plausible/community-edition.git plausible
cd plausible

# Generate secret key
openssl rand -base64 64 | tr -d '\n'

# Edit plausible-conf.env with the generated key

# Start the services
docker compose up -d

# Check logs
docker compose logs -f plausible

# Plausible will be available at http://localhost:8000

Nginx reverse proxy

Code
NGINX
# /etc/nginx/sites-available/plausible
server {
    listen 80;
    server_name analytics.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name analytics.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/analytics.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/analytics.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location = /js/script.js {
        proxy_pass http://127.0.0.1:8000/js/script.js;
        proxy_set_header Host $host;
        proxy_cache_valid 200 1h;
        add_header Cache-Control "public, max-age=3600";
    }
}

Proxying the script through your own server

To avoid being blocked by ad blockers:

TSapp/api/script.js/route.ts
TypeScript
// app/api/script.js/route.ts (Next.js)
export async function GET() {
  const response = await fetch('https://plausible.io/js/script.js')
  const script = await response.text()

  const modifiedScript = script.replace(
    'https://plausible.io/api/event',
    'https://yourdomain.com/api/event'
  )

  return new Response(modifiedScript, {
    headers: {
      'Content-Type': 'application/javascript',
      'Cache-Control': 'public, max-age=3600'
    }
  })
}

// app/api/event/route.ts
export async function POST(request: Request) {
  const body = await request.text()

  await fetch('https://plausible.io/api/event', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent': request.headers.get('user-agent') || '',
      'X-Forwarded-For': request.headers.get('x-forwarded-for') || ''
    },
    body
  })

  return new Response('ok')
}

Framework integrations

next-plausible

Code
Bash
npm install next-plausible
TSapp/layout.tsx
TypeScript
// app/layout.tsx
import PlausibleProvider from 'next-plausible'

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <PlausibleProvider
          domain="yourdomain.com"
          trackOutboundLinks
          trackFileDownloads
          taggedEvents
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

'use client'

import { usePlausible } from 'next-plausible'

function MyComponent() {
  const plausible = usePlausible()

  const handleClick = () => {
    plausible('Button Click', {
      props: { location: 'header', variant: 'primary' }
    })
  }

  return <button onClick={handleClick}>Click me</button>
}

@plausible/tracker

Code
Bash
npm install @plausible/tracker
TSlib/plausible.ts
TypeScript
// lib/plausible.ts
import Plausible from '@plausible/tracker'

export const plausible = Plausible({
  domain: 'yourdomain.com',
  trackLocalhost: true,
  apiHost: 'https://plausible.io'
})

import { plausible } from '@/lib/plausible'

plausible.trackEvent('Signup', {
  props: { plan: 'pro' }
})

plausible.enableAutoPageviews()
plausible.enableAutoOutboundTracking()

Vue.js plugin

TSplugins/plausible.ts
TypeScript
// plugins/plausible.ts
import { createPlausible } from 'vue-plausible'

export default defineNuxtPlugin((nuxtApp) => {
  const plausible = createPlausible({
    domain: 'yourdomain.com',
    trackLocalhost: false
  })

  nuxtApp.vueApp.use(plausible)
})

<script setup>
import { usePlausible } from 'vue-plausible'

const { trackEvent } = usePlausible()

function handleClick() {
  trackEvent('Button Click')
}
</script>

Pricing

Billing follows monthly pageviews, and the plans differ in feature scope at the same traffic threshold. That distinction gets muddled, because many roundups quote a single price as though only one plan existed.

PlanFromWhat it adds
Starter9 USD per monthcore reports, up to 10k pageviews
Growth14 USD per monthmore team members, shared views
Business19 USD per monthfunnels, revenue data, API access
Enterprisecustom quoteabove 10M pageviews per month

The price rises with the pageview threshold, and paying annually gives two months free. The trial runs thirty days and needs no payment card.

One thing is worth noticing when comparing with competitors. Billing by pageviews rather than sessions or users means the cost rises with bot traffic unless it is filtered out. For a site attracting many automated visits, check actual consumption before picking a threshold.

The self hosted edition

The community edition is free under AGPL 3.0 with no pageview cap. You pay only for the server.

Here lies the most common misconception about this product, though, so it needs stating plainly: the free edition is not equivalent to the cloud. Funnels, Google Analytics data import, and team single sign on are available in the paid service only. If any of those matter to you, self hosting will not substitute for them.

Hardware requirements are modest: two gigabytes of memory and twenty gigabytes of disk suffice to start, growing with event history. Running it needs Docker and a domain with a certificate.

Cost it honestly, though. A server at a few dollars a month plus time for upgrades and backups rarely comes out cheaper than nine dollars for the entry plan. Self hosting pays off at high traffic or under a requirement that data physically never leave your infrastructure.

FAQ - frequently asked questions

Does Plausible really not use cookies?

Yes, Plausible does not use any cookies or localStorage. Unique visit identification relies on hashing: IP + User Agent + domain + date. The hash is regenerated every day, so there is no way to track a user across days.

Do I need to display a GDPR banner?

No, if you are only using Plausible. Since Plausible does not collect personal data and does not use cookies, it does not require user consent under GDPR. However, if you use other tools that require consent, you will still need the banner.

How does Plausible handle ad blockers?

The default Plausible script is blocked by some ad blockers. Solutions:

  1. Proxy the script through your own server
  2. Use a custom subdomain (analytics.yourdomain.com)
  3. Self-host the entire instance

Can I use Plausible on multiple domains?

Yes, each domain is a separate "site" in the dashboard. You can also aggregate data from multiple subdomains on a single dashboard.

How long does Plausible store data?

Plausible Cloud stores data indefinitely -- there is no retention limit. In the self-hosted version, you can configure your own retention policies.

Does Plausible track subdomains?

You can configure Plausible to track:

  • Only the main domain
  • All subdomains together
  • Each subdomain separately
Code
HTML
<!-- All subdomains together -->
<script data-domain="yourdomain.com" src="..."></script>

<!-- Specific subdomain -->
<script data-domain="blog.yourdomain.com" src="..."></script>

How to migrate from Google Analytics?

  1. Add the Plausible script alongside GA
  2. Compare data for 2-4 weeks
  3. Define goals matching your GA events
  4. Remove the GA script after verification

Plausible also offers historical data import from Google Analytics.

Can I use Plausible to track mobile apps?

This tool is designed primarily for websites. For mobile apps, something oriented around product events makes more sense, PostHog for instance. Events can be sent from an application through the API, but the interface itself is built around web traffic.

How does the free edition differ from the cloud?

The community edition is not equivalent to the paid service. It lacks funnels, Google Analytics data import, and team single sign on. This is the most common misconception about the product.

What this analytics will not tell you

This section matters more than a feature list, because it decides whether the tool fits your questions at all.

You will not learn what a particular user did, unlike tools collecting product events, or Sentry on the error side. The absence of a persistent identifier means no path for one person across several days. If your question is "why did this customer abandon their cart", you need a product analytics tool, not this category.

Nor will you see retention or cohorts in the sense product analytics means. You can check how many visitors returned, but you cannot follow how a group that signed up in a given week behaves afterwards.

The third thing is visitor count accuracy, which matters when reconciling with other tools. Recognition based on a hash of address and browser, refreshed daily, means the same person visiting on Monday and Tuesday counts twice. Compared against Google Analytics that difference is often mistaken for a bug, when it follows directly from declining to tag people persistently.

The fourth is blocking. Although the script is light and unobtrusive, some ad blockers stop it anyway, so numbers will be understated. The scale depends on who visits you: on a technical site losses run into double digits, on a mainstream site they are usually marginal. The remedy is proxying the script through your own domain, which does require configuration on the Next.js side or at the network layer.

The fifth, worth knowing when deciding: there are no session recordings and no click maps. That is a deliberate choice following from the privacy stance rather than a gap awaiting a future release.

The source and releases live in the project repository, and current plans on the pricing page.