plausible/analytics

Open source, privacy-first web analytics. Lightweight, cookie-free Google Analytics alternative. Self-hosted or cloud.

24,628 stars Elixir 10 components

Collects and displays real-time web analytics without cookies or personal data

Website visitors trigger tracking events from injected JavaScript which POST to Phoenix controllers for validation and queuing. Background workers enrich events with geolocation and user agent data before batch insertion into ClickHouse. Dashboard users request analytics data through React components that query Phoenix API endpoints, which aggregate ClickHouse data and return JSON formatted for chart and table visualization.

Under the hood, the system uses 3 feedback loops, 5 data pools, 5 control points to manage its runtime behavior.

A 10-component fullstack. 1417 files analyzed. Data flows through 6 distinct pipeline stages.

How Data Flows Through the System

Website visitors trigger tracking events from injected JavaScript which POST to Phoenix controllers for validation and queuing. Background workers enrich events with geolocation and user agent data before batch insertion into ClickHouse. Dashboard users request analytics data through React components that query Phoenix API endpoints, which aggregate ClickHouse data and return JSON formatted for chart and table visualization.

  1. Track website visitor events — PlausibleTracker JavaScript on client websites captures page views and custom events, validates against configured domains, and sends HTTP POST requests to /api/event with visitor data but no persistent cookies
  2. Ingest and validate tracking events — IngestController receives tracking POSTs, validates referrer domains against site allowlists, extracts IP addresses for geolocation without storing them, and queues validated events for background processing [HTTP tracking request → Queued tracking event]
  3. Enrich and store analytics events — EventWorker background jobs dequeue tracking events, parse user agent strings for browser/OS data, perform IP geolocation lookups, then batch insert enriched events into ClickHouse events table with session tracking [Queued tracking event → ClickHouse event records]
  4. Request dashboard analytics data — React dashboard components trigger API requests through the api.get() function, serializing DashboardState filters and time periods into query parameters for Phoenix StatsController endpoints [DashboardState → HTTP API request]
  5. Query and aggregate analytics — StatsController validates request parameters, constructs ClickHouse queries through Stats.Query with date ranges and WHERE clauses from filters, executes aggregation queries, and formats results into QueryApiResponse JSON [HTTP API request → QueryApiResponse]
  6. Render dashboard visualizations — React components like VisitorGraph and ListReport consume QueryApiResponse data to render time series charts using Chart.js and tabular reports with Metric formatters, updating the dashboard UI with current analytics [QueryApiResponse → Dashboard UI updates]

Data Models

The data structures that flow between stages — the contracts that hold the system together.

DashboardState assets/js/dashboard/dashboard-state.ts
TypeScript interface with period: DashboardPeriod, filters: Filter[] (triplets of [operator, key, clauses]), date/from/to: Dayjs objects, comparison: ComparisonMode, labels: FilterClauseLabels (ID to human-readable mappings)
Created from URL params and localStorage on page load, updated through user interactions, serialized into API requests and browser navigation
PlausibleSite assets/js/dashboard/site-context.tsx
TypeScript object with domain: string, offset: number (timezone), feature flags like hasGoals/hasProps/funnelsAvailable: boolean, revenueGoals/funnels: arrays, statsBegin/nativeStatsBegin: date strings
Parsed from server-rendered data attributes on initial page load, passed through React context to all dashboard components
Metric assets/js/dashboard/stats/reports/metrics.js
JavaScript class with key: string (API field name), formatter: function (value renderer), renderLabel: function (contextual labels), width: CSS class, meta: object (display hints like plot: true)
Created via factory functions like createVisitors(), configured with dashboard state context, used to render values and labels in tables and charts
QueryApiResponse assets/js/dashboard/api.ts
TypeScript interface with query: {metrics: Metric[], date_range: [string, string]}, results: Array<{metrics: number[], dimensions: string[], comparison: {metrics: number[], change: number[]}}>, meta: Record<string, unknown>
Returned from backend API calls, processed by dashboard components to extract time series data and comparison metrics for visualization
AuditEntry extra/lib/plausible/audit/entry.ex
Ecto schema with name: String.t(), entity/entity_id: String.t(), change: map() (encoded diff), user_id/team_id: integer(), datetime: NaiveDateTime.t(), actor_type: :system | :user
Created automatically when tracked entities change, encoded using Audit.Encoder protocol, persisted to audit_entries table, queried for compliance reporting

Hidden Assumptions

Things this code relies on but never validates. These are the things that cause silent failures when the system changes.

critical Shape unguarded

Mouse events have target property that points to DOM elements that respond to contains() method - assumes e.target is a node and this.node.current is a DOM element with contains method

If this fails: If event target is null, undefined, or doesn't have contains method, modal throws TypeError and breaks click-outside-to-close functionality

assets/js/dashboard/stats/modals/modal.js:handleClickOutside
warning Environment unguarded

Cross-origin fetch to https://plausible.io/changes.txt will always succeed and return valid text response - assumes network availability, CORS headers, and server uptime

If this fails: Network failures or CORS issues cause silent Promise rejection, leaving changelog notification in inconsistent state with no error handling

assets/js/app.js:changelogNotification
critical Shape unguarded

All changeset.changes values can be recursively encoded without circular references - assumes data structures are acyclic trees

If this fails: Circular references in changeset data cause infinite recursion and stack overflow during audit logging, potentially crashing the process

extra/lib/plausible/audit/encoder.ex:encode/2
warning Domain weakly guarded

Browser names from user agent parsing exactly match BROWSER_ICONS keys - assumes consistent string formatting between user agent parser and icon mapping

If this fails: Unrecognized browser names fall back to 'fallback.svg' which may not exist, causing broken image icons in device reports

assets/js/dashboard/stats/devices/index.js:browserIconFor
info Temporal weakly guarded

localStorage.lastChangelogUpdate contains valid timestamp number that can be parsed - assumes previous successful fetch stored valid Date.getTime() value

If this fails: Invalid or corrupted timestamp values cause NaN comparisons, potentially showing changelog notification incorrectly or never

assets/js/app.js:showChangelogNotification
critical Environment unguarded

DOM document.body exists and supports style property modification - assumes browser environment with full DOM API availability

If this fails: In server-side rendering or non-browser environments, accessing document.body.style throws reference error and prevents modal rendering

assets/js/dashboard/stats/modals/modal.js:componentDidMount
warning Ordering weakly guarded

parseSearch(location.search) returns object with expected filter structure before postProcessFilters runs - assumes URL parsing happens before filter processing

If this fails: Malformed URL search params could pass undefined or invalid filter arrays to postProcessFilters, causing dashboard state corruption

assets/js/dashboard/dashboard-state-context.tsx:useMemo
warning Scale unguarded

Goal names in localStorage keys are URL-safe and don't contain special characters - assumes goal names from API responses are sanitized for storage key usage

If this fails: Goal names with special characters create invalid localStorage keys, causing prop key storage/retrieval to fail silently for those goals

assets/js/dashboard/stats/behaviours/index.js:STORAGE_KEYS.getForPropKeyForGoal
warning Resource weakly guarded

Window innerWidth is available and returns valid number - assumes browser window object exists with width measurement capability

If this fails: In headless or server environments where window.innerWidth is undefined, modal sizing falls back to DEFAULT_WIDTH but resize handler may throw errors

assets/js/dashboard/stats/modals/modal.js:DEFAULT_WIDTH
critical Contract unguarded

ListReport component expects getFilterInfo to return object with specific shape {prefix: string, filter: [string, string, string[]]} - assumes consistent filter structure contract

If this fails: If getFilterInfo returns different structure, ListReport filter application fails silently or throws errors when constructing dashboard queries

assets/js/dashboard/stats/pages/index.js:getFilterInfo

System Behavior

How the system operates at runtime — where data accumulates, what loops, what waits, and what controls what.

Data Pools

ClickHouse Events (database)
Time-series storage for all website tracking events with columns for timestamp, page, referrer, browser, location - optimized for analytics aggregations
PostgreSQL Sites (database)
Relational data for user accounts, site configurations, goals, funnels, and audit trails - handles authentication and feature management
Event Processing Queue (queue)
Background job queue for tracking events awaiting enrichment and database insertion - handles traffic spikes and processing delays
Browser Local Storage (cache)
Client-side persistence for dashboard preferences like selected tabs, time periods, and property keys - survives page refreshes
React Component State (in-memory)
Centralized dashboard state holding current filters, time periods, and comparison settings - synchronized with URL and drives all queries

Feedback Loops

Delays

Control Points

Technology Stack

Elixir/Phoenix (framework)
Web framework providing HTTP controllers, real-time channels, and OTP supervision for concurrent event processing
React (framework)
Dashboard UI library with TypeScript, context providers, and functional components for analytics visualization
ClickHouse (database)
Columnar database optimized for analytics queries and time-series data storage with event aggregation
PostgreSQL (database)
Relational database for user accounts, site configuration, goals, and audit trail persistence
Oban (library)
Background job processing for async event enrichment and batch database operations
Chart.js (library)
Time series visualization for visitor graphs and metrics dashboards
Ecto (library)
Database ORM providing schema definitions, changesets, and query composition for PostgreSQL
Alpine.js (library)
Lightweight JavaScript for server-rendered page interactivity in non-dashboard interfaces

Key Components

Explore the interactive analysis

See the full architecture map, data flow, and code patterns visualization.

Analyze on CodeSea

Related Fullstack Repositories

Frequently Asked Questions

What is analytics used for?

Collects and displays real-time web analytics without cookies or personal data plausible/analytics is a 10-component fullstack written in Elixir. Data flows through 6 distinct pipeline stages. The codebase contains 1417 files.

How is analytics architected?

analytics is organized into 5 architecture layers: Tracking Scripts, Ingestion Layer, Analytics Engine, Dashboard UI, and 1 more. Data flows through 6 distinct pipeline stages. This layered structure keeps concerns separated and modules independent.

How does data flow through analytics?

Data moves through 6 stages: Track website visitor events → Ingest and validate tracking events → Enrich and store analytics events → Request dashboard analytics data → Query and aggregate analytics → .... Website visitors trigger tracking events from injected JavaScript which POST to Phoenix controllers for validation and queuing. Background workers enrich events with geolocation and user agent data before batch insertion into ClickHouse. Dashboard users request analytics data through React components that query Phoenix API endpoints, which aggregate ClickHouse data and return JSON formatted for chart and table visualization. This pipeline design reflects a complex multi-stage processing system.

What technologies does analytics use?

The core stack includes Elixir/Phoenix (Web framework providing HTTP controllers, real-time channels, and OTP supervision for concurrent event processing), React (Dashboard UI library with TypeScript, context providers, and functional components for analytics visualization), ClickHouse (Columnar database optimized for analytics queries and time-series data storage with event aggregation), PostgreSQL (Relational database for user accounts, site configuration, goals, and audit trail persistence), Oban (Background job processing for async event enrichment and batch database operations), Chart.js (Time series visualization for visitor graphs and metrics dashboards), and 2 more. A focused set of dependencies that keeps the build manageable.

What system dynamics does analytics have?

analytics exhibits 5 data pools (ClickHouse Events, PostgreSQL Sites), 3 feedback loops, 5 control points, 3 delays. The feedback loops handle polling and retry. These runtime behaviors shape how the system responds to load, failures, and configuration changes.

What design patterns does analytics use?

5 design patterns detected: Context Providers, Generic Report Components, Protocol-based Encoding, Background Job Processing, Modal Routing.

Analyzed on April 20, 2026 by CodeSea. Written by .