How Plausible Works
Plausible collects analytics through a single JavaScript file (script.js, under 1KB gzipped) that sends POST requests to /api/event. Each request contains the page URL, referrer, screen width, and a user agent string. Plausible does not set cookies and does not use localStorage or fingerprinting.
Visitor uniqueness is determined by hashing the visitor's IP address, User-Agent, and the website domain with a rotating daily salt. Because the salt rotates every 24 hours at midnight UTC, the same visitor on two different days generates two different hashes. This means Plausible cannot track returning visitors across days -- a deliberate privacy constraint.
The data model has three core concepts:
- Pageviews -- Each page load or client-side navigation event
- Visits (Sessions) -- A group of pageviews from the same hashed identifier within a session window
- Custom Events -- Named actions with optional properties attached to a pageview context
All data is stored in ClickHouse (self-hosted) or Plausible's managed ClickHouse cluster (cloud). There is no concept of individual visitor profiles or user IDs.
Installing the Tracking Script
Add the script tag to the <head> of every page:
<script defer data-domain="example.com"
src="https://plausible.io/js/script.js"></script>
For self-hosted instances, replace plausible.io with your own domain.
The data-domain attribute must match the site domain registered in your Plausible dashboard. It controls which site the data is attributed to.
Script Extensions
Plausible uses filename-based configuration. Append extensions to the script filename to enable features:
<!-- Track outbound link clicks -->
<script defer data-domain="example.com"
src="https://plausible.io/js/script.outbound-links.js"></script>
<!-- Track file downloads -->
<script defer data-domain="example.com"
src="https://plausible.io/js/script.file-downloads.js"></script>
<!-- Hash-based routing (for SPAs using # routes) -->
<script defer data-domain="example.com"
src="https://plausible.io/js/script.hash.js"></script>
<!-- Multiple extensions combined -->
<script defer data-domain="example.com"
src="https://plausible.io/js/script.hash.outbound-links.file-downloads.js"></script>
Available extensions: hash, outbound-links, file-downloads, tagged-events, revenue, pageview-props, compat (for IE compatibility).
SPA Tracking
Plausible automatically tracks pushState-based navigation in frameworks like React Router, Next.js, and Vue Router. No extra configuration is needed for SPAs that use the History API.
For hash-based routing (/#/page), add the hash extension to the script filename.
Manual Pageview Tracking
If you need control over when pageviews fire:
<script defer data-domain="example.com"
src="https://plausible.io/js/script.manual.js"></script>
Then trigger pageviews in your code:
// Trigger a pageview
plausible('pageview');
// Track a virtual page (SPA or custom URL)
plausible('pageview', { u: 'https://example.com/virtual-page' });
Custom Events and Goal Conversions
Sending Custom Events
Custom events are tracked via the plausible() function that the script exposes on window:
// Basic event
plausible('Signup');
// Event with custom properties
plausible('Purchase', {
props: {
plan: 'Pro',
amount: '49'
}
});
// Event with revenue tracking (requires revenue extension)
plausible('Purchase', {
revenue: { currency: 'USD', amount: '49.99' }
});
// Callback after event is sent
plausible('Download', {
callback: () => console.log('Event sent')
});
Event names are case-sensitive. Custom properties are limited to string values. Revenue tracking requires the revenue script extension.
Tagged Events (CSS-Based)
Instead of writing JavaScript, use CSS classes and data attributes:
<script defer data-domain="example.com"
src="https://plausible.io/js/script.tagged-events.js"></script>
<!-- Tracked automatically on click -->
<a href="/signup" class="plausible-event-name=Signup">Sign Up</a>
<!-- With custom properties -->
<button class="plausible-event-name=CTA+Click plausible-event-position=Hero">
Get Started
</button>
Spaces in event names use + in the class value.
Goal Configuration
Goals are configured in the Plausible dashboard under Site Settings > Goals. Types:
- Pageview goals -- Triggered when a specific URL path is visited. Supports wildcards:
/blog/* - Custom event goals -- Triggered by
plausible('EventName')calls. Must match the event name exactly.
Once a goal is configured, it appears in the dashboard with conversion rate and referrer attribution.
Pageview Properties
Attach metadata to pageviews (requires pageview-props extension):
plausible('pageview', {
props: {
author: 'Jane Doe',
category: 'Engineering'
}
});
Self-Hosted Deployment
Plausible self-hosted runs as a Docker Compose stack with three services: the Plausible app (Elixir), ClickHouse (analytics database), and PostgreSQL (configuration and user accounts).
Docker Compose Setup
Clone the hosting repo and configure:
git clone https://github.com/plausible/community-edition plausible-ce
cd plausible-ce
Create a plausible-conf.env file:
BASE_URL=https://analytics.example.com
SECRET_KEY_BASE=$(openssl rand -base64 48)
TOTP_VAULT_KEY=$(openssl rand -base64 32)
MAXMIND_LICENSE_KEY=your_key # Optional, for geolocation
The docker-compose.yml ships with the repo. Start with:
docker compose up -d
Environment Variables
Key configuration variables for self-hosted:
| Variable | Default | Description |
|---|---|---|
BASE_URL |
required | Public URL of your Plausible instance |
SECRET_KEY_BASE |
required | 64+ char secret for signing |
DATABASE_URL |
postgres://postgres:postgres@plausible_db:5432/plausible_db |
PostgreSQL connection |
CLICKHOUSE_DATABASE_URL |
http://plausible_events_db:8123/plausible_events_db |
ClickHouse connection |
DISABLE_REGISTRATION |
invite_only |
Set to true after initial setup |
MAXMIND_LICENSE_KEY |
none | For IP geolocation |
GOOGLE_CLIENT_ID |
none | For Google Search Console integration |
MAILER_EMAIL |
none | From address for emails |
SMTP_HOST_ADDR |
none | SMTP server |
Reverse Proxy
Place behind Nginx or Caddy for TLS:
# Caddy
analytics.example.com {
reverse_proxy localhost:8000
}
Upgrading
docker compose pull
docker compose up -d
Plausible runs database migrations automatically on startup.
Proxy Setup for Ad-Blocker Bypass
Ad blockers commonly block requests to plausible.io. Proxy the script and event endpoint through your own domain to avoid this.
Nginx Proxy
location = /js/script.js {
proxy_pass https://plausible.io/js/script.js;
proxy_set_header Host plausible.io;
proxy_ssl_server_name on;
}
location = /api/event {
proxy_pass https://plausible.io/api/event;
proxy_set_header Host plausible.io;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_ssl_server_name on;
}
Then update the script tag to point at your domain:
<script defer data-domain="example.com"
data-api="/api/event"
src="/js/script.js"></script>
The data-api attribute tells the tracker where to send events. Both attributes must be set when proxying.
Cloudflare Worker Proxy
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/js/script.js') {
return fetch('https://plausible.io/js/script.js');
}
if (url.pathname === '/api/event') {
const newRequest = new Request('https://plausible.io/api/event', {
method: request.method,
headers: request.headers,
body: request.body,
});
newRequest.headers.set('X-Forwarded-For',
request.headers.get('CF-Connecting-IP'));
return fetch(newRequest);
}
return new Response('Not found', { status: 404 });
}
};
Next.js Rewrites
// 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',
},
];
},
};
Stats API
The Plausible Stats API is read-only and returns aggregate data. Authenticate with a bearer token generated in Account Settings > API Keys.
Aggregate Endpoint
curl "https://plausible.io/api/v1/stats/aggregate?\
site_id=example.com&\
period=30d&\
metrics=visitors,pageviews,bounce_rate,visit_duration" \
-H "Authorization: Bearer YOUR_API_KEY"
Response:
{
"results": {
"visitors": { "value": 12543 },
"pageviews": { "value": 31209 },
"bounce_rate": { "value": 42.3 },
"visit_duration": { "value": 187 }
}
}
Time Series Endpoint
curl "https://plausible.io/api/v1/stats/timeseries?\
site_id=example.com&\
period=30d&\
metrics=visitors,pageviews&\
interval=date" \
-H "Authorization: Bearer YOUR_API_KEY"
Breakdown Endpoint
# Top pages by visitors
curl "https://plausible.io/api/v1/stats/breakdown?\
site_id=example.com&\
period=30d&\
property=event:page&\
metrics=visitors,pageviews&\
limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"
# Breakdown by custom event property
curl "https://plausible.io/api/v1/stats/breakdown?\
site_id=example.com&\
period=30d&\
property=event:props:plan&\
filters=event:name==Purchase" \
-H "Authorization: Bearer YOUR_API_KEY"
Filtering
Apply filters to any endpoint:
# Only mobile visitors from Google
&filters=visit:source==Google;visit:device==Mobile
# Pages matching a pattern
&filters=event:page==/blog/**
# Custom event property
&filters=event:props:plan==Pro
Filter operators: == (equals), != (not equals), * (wildcard), | (OR within a value).
Available Properties
event:page, event:name, visit:source, visit:referrer, visit:utm_medium, visit:utm_source, visit:utm_campaign, visit:utm_content, visit:utm_term, visit:device, visit:browser, visit:browser_version, visit:os, visit:os_version, visit:country, visit:region, visit:city, visit:entry_page, visit:exit_page.
Common Issues
Script blocked by ad blockers: Set up a first-party proxy as described in the proxy section. This is the most common reason for undercounting visitors.
Hash-based SPA routes not tracked:
Add the hash extension to the script filename. Without it, changes to the URL hash fragment are ignored.
Custom events not appearing in dashboard:
Goals must be created in the Plausible dashboard before event data shows up. The event name in plausible('EventName') must exactly match the goal name (case-sensitive).
Pageview counts differ from server logs: Plausible only counts visits from real browsers. Bots, crawlers, and prefetch requests are filtered. The daily rotating salt also means visitors who block JavaScript or leave before the script loads are not counted.
Self-hosted geolocation not working:
Set MAXMIND_LICENSE_KEY in your environment. Plausible downloads the GeoLite2 database on startup. Without it, all visitors show as "Unknown" country.
plausible() function not defined:
The script loads with defer, so it is not available until after DOM parsing completes. Wrap calls in a DOMContentLoaded listener or place scripts after the Plausible script tag.
Platform-Specific Considerations
ClickHouse Storage: Self-hosted Plausible stores all event data in ClickHouse. For sites with 1M+ monthly pageviews, allocate at least 2GB RAM for ClickHouse. Data compression is aggressive -- expect roughly 30-50 bytes per event on disk.
No Raw Data Export: Plausible does not expose individual pageview records. The Stats API returns aggregated data only. For raw event access on self-hosted, query ClickHouse directly.
Multi-Domain Tracking:
Track events across multiple subdomains by listing all domains in data-domain:
<script defer
data-domain="example.com,app.example.com"
src="https://plausible.io/js/script.js"></script>
Roll-up reporting aggregates data across all listed domains in the dashboard.
Server-Side Event Ingestion: Send events without JavaScript by POSTing directly to the event API:
curl -X POST https://plausible.io/api/event \
-H "User-Agent: Mozilla/5.0..." \
-H "X-Forwarded-For: 203.0.113.1" \
-H "Content-Type: application/json" \
-d '{
"name": "pageview",
"url": "https://example.com/api-tracked-page",
"domain": "example.com"
}'
The User-Agent and X-Forwarded-For headers are required for visitor identification. Without them, the event is rejected.