plausible/analytics
Open source, privacy-first web analytics. Lightweight, cookie-free Google Analytics alternative. Self-hosted or cloud.
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.
- 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
- 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]
- 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]
- 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]
- 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]
- 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.
assets/js/dashboard/dashboard-state.tsTypeScript 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
assets/js/dashboard/site-context.tsxTypeScript 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
assets/js/dashboard/stats/reports/metrics.jsJavaScript 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
assets/js/dashboard/api.tsTypeScript 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
extra/lib/plausible/audit/entry.exEcto 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.
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
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
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
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
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
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
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
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
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
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
Time-series storage for all website tracking events with columns for timestamp, page, referrer, browser, location - optimized for analytics aggregations
Relational data for user accounts, site configurations, goals, funnels, and audit trails - handles authentication and feature management
Background job queue for tracking events awaiting enrichment and database insertion - handles traffic spikes and processing delays
Client-side persistence for dashboard preferences like selected tabs, time periods, and property keys - survives page refreshes
Centralized dashboard state holding current filters, time periods, and comparison settings - synchronized with URL and drives all queries
Feedback Loops
- Real-time visitor updates (polling, reinforcing) — Trigger: Dashboard showing current visitors. Action: React components poll /api/stats endpoints every 30 seconds to refresh current visitor counts. Exit: User switches to historical time period.
- Failed event retry (retry, balancing) — Trigger: EventWorker job fails during processing. Action: Oban automatically retries failed jobs with exponential backoff up to maximum attempts. Exit: Job succeeds or exceeds retry limit.
- Dashboard URL synchronization (self-correction, balancing) — Trigger: User changes filters or time period. Action: DashboardStateContext updates URL params and localStorage, triggering new API queries that refresh all dashboard components. Exit: All components render updated data.
Delays
- Event processing latency (async-processing, ~1-30 seconds) — Tracking events appear in dashboard analytics after background enrichment and batch insertion
- Dashboard data freshness (cache-ttl, ~30 seconds) — Analytics queries may return cached results to reduce ClickHouse load during high traffic
- Real-time polling interval (scheduled-job, ~30 seconds) — Current visitor counts update every 30 seconds when dashboard shows real-time data
Control Points
- License validation (runtime-toggle) — Controls: Enterprise features availability and application startup - validates license key hash against hardcoded value. Default: Production environment check
- Feature flags (feature-flag) — Controls: Dashboard feature availability like funnels, props, exploration based on site configuration and subscription tier. Default: Site-specific boolean flags
- Batch insertion size (env-var) — Controls: Number of events processed in each background job batch - affects ingestion throughput and memory usage
- API rate limits (rate-limit) — Controls: Maximum requests per IP per time window for both tracking and dashboard APIs
- Real-time threshold (threshold) — Controls: When dashboard switches between historical analytics and real-time current visitors display. Default: Period = 'realtime'
Technology Stack
Web framework providing HTTP controllers, real-time channels, and OTP supervision for concurrent event processing
Dashboard UI library with TypeScript, context providers, and functional components for analytics visualization
Columnar database optimized for analytics queries and time-series data storage with event aggregation
Relational database for user accounts, site configuration, goals, and audit trail persistence
Background job processing for async event enrichment and batch database operations
Time series visualization for visitor graphs and metrics dashboards
Database ORM providing schema definitions, changesets, and query composition for PostgreSQL
Lightweight JavaScript for server-rendered page interactivity in non-dashboard interfaces
Key Components
- DashboardStateContextProvider (orchestrator) — Manages all dashboard query state derived from URL params, localStorage preferences, and user interactions — coordinates filtering, time periods, and comparisons across the entire analytics interface
assets/js/dashboard/dashboard-state-context.tsx - VisitorGraph (processor) — Renders the main time series chart showing visitor counts, page views, and conversion metrics over the selected time period with comparison data and real-time updates
assets/js/dashboard/stats/graph/visitor-graph.tsx - ListReport (processor) — Generic table component that fetches API data, renders metrics in columns with sorting and filtering, handles pagination, and provides drill-down navigation — used for pages, sources, locations, devices
assets/js/dashboard/stats/reports/list.js - StatsController (gateway) — Phoenix API controller that validates dashboard requests, converts filter parameters, queries ClickHouse through Stats.Query, and returns JSON formatted for the React frontend
lib/plausible_web/controllers/api/stats_controller.ex - Stats.Query (processor) — Constructs and executes ClickHouse SQL queries from dashboard filter state, handles time period logic, comparison queries, and metric aggregations
lib/plausible/stats/query.ex - IngestController (gateway) — Receives tracking events from website JavaScript, validates referrer domains, processes IP addresses for geolocation, and queues events for batch database insertion
lib/plausible_web/controllers/api/external_controller.ex - EventWorker (processor) — Background job that takes queued tracking events, enriches them with user agent parsing and geolocation data, then batch inserts into ClickHouse events table
lib/workers/event_worker.ex - Modal (adapter) — Base React component that renders drill-down overlays with keyboard navigation, handles click-outside-to-close, manages body scroll locking, and provides responsive sizing
assets/js/dashboard/stats/modals/modal.js - PlausibleTracker (encoder) — Client-side JavaScript that automatically captures page views and custom events, sends them to the ingestion API without cookies, and handles single-page application navigation
tracker/src/plausible.js - AuditEncoder (serializer) — Protocol implementation that recursively encodes Ecto changesets and data structures into audit-friendly maps, handling before/after state comparisons and sensitive data filtering
extra/lib/plausible/audit/encoder.ex
Explore the interactive analysis
See the full architecture map, data flow, and code patterns visualization.
Analyze on CodeSeaRelated 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 Karolina Sarna.