Plausible Analytics React Frontend Architecture and Backend Communication

Plausible Analytics implements a React/TypeScript single-page application that communicates with an Elixir Phoenix backend via RESTful JSON API endpoints, utilizing context providers for global state management and a thin fetch-based utility layer located in assets/js/dashboard/util/api.ts.

The plausible/analytics repository delivers a modern, privacy-focused analytics dashboard through a decoupled architecture. This React frontend architecture separates the user interface in assets/js/dashboard/ from the data aggregation engine, enabling lightweight static hosting for the UI while the Phoenix backend handles complex database queries.

Frontend Structure: Component-Based React SPA

The dashboard is organized as a modular single-page application built with React and TypeScript. It follows a layered architecture with clear separation between state management, UI components, and data fetching concerns.

Application Bootstrap

The entry point assets/js/app.js initializes the React application and mounts it into the DOM. It renders the root <Dashboard /> component into a specific HTML element identified by the ID plausible-dashboard.

import React from 'react';
import { createRoot } from 'react-dom/client';
import Dashboard from './dashboard';

const root = createRoot(document.getElementById('plausible-dashboard')!);
root.render(<Dashboard />);

Source: [assets/js/app.js](https://github.com/plausible/analytics/blob/master/assets/js/app.js)

Dashboard Component Hierarchy

The Dashboard component in assets/js/dashboard/index.tsx serves as the main shell, orchestrating the layout and wrapping child components in multiple context providers. It conditionally renders elements like the top bar based on whether the dashboard is in real-time mode.

function Dashboard() {
  const isRealTimeDashboard = useIsRealtimeDashboard();
  const [importedDataInView, setImportedDataInView] = useState(false);

  return (
    <CurrentVisitorsProvider>
      <GraphIntervalProvider>
        <ImportsIncludedProvider>
          <div className="mb-16 grid grid-cols-1 md:grid-cols-2 gap-5">
            <TopBar showCurrentVisitors={!isRealTimeDashboard} />
            <DashboardStats
              importedDataInView={isRealTimeDashboard ? undefined : importedDataInView}
              updateImportedDataInView={isRealTimeDashboard ? undefined : setImportedDataInView}
            />
          </div>
        </ImportsIncludedProvider>
      </GraphIntervalProvider>
    </CurrentVisitorsProvider>
  );
}

Source: [assets/js/dashboard/index.tsx](https://github.com/plausible/analytics/blob/master/assets/js/dashboard/index.tsx)

State Management via Context Providers

Global state is managed through React Context rather than external libraries like Redux. Key providers include:

  • DashboardStateContext – Stores the selected time period, site ID, and other dashboard-wide settings
  • CurrentVisitorsContext – Manages real-time visitor counts
  • GraphIntervalContext – Controls chart granularity and intervals
  • ImportsIncludedProvider – Toggles inclusion of imported historical data

These providers enable deep child components to access shared state without prop drilling, triggering re-renders across the component tree when data updates.

Backend Communication and API Design

The frontend communicates with the Elixir Phoenix backend through a RESTful JSON API. This decoupled approach allows the static React application to query analytics data dynamically without server-side rendering dependencies.

The Fetch Wrapper

All HTTP requests are centralized in assets/js/dashboard/util/api.ts. This thin abstraction wraps window.fetch to standardize header injection, URL construction, and error handling. It automatically includes the Plausible site ID and authentication credentials with each request.

export async function getStats(endpoint: string, params: Record<string, string>) {
  const url = new URL(`/api/v1/${endpoint}`, window.location.origin);
  Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v));

  const resp = await fetch(url.toString(), {
    headers: { 'Accept': 'application/json' },
    credentials: 'same-origin',
  });

  if (!resp.ok) {
    throw new Error(`API error ${resp.status}`);
  }
  return resp.json();
}

Source: [assets/js/dashboard/util/api.ts](https://github.com/plausible/analytics/blob/master/assets/js/dashboard/util/api.ts)

Phoenix API Controllers

The backend implementation resides in lib/plausible_web/controllers/api/. The StatsController serves aggregated analytics data, querying PostgreSQL and returning formatted JSON responses.

defmodule PlausibleWeb.Api.StatsController do
  use PlausibleWeb, :controller

  def show(conn, %{"site_id" => site_id, "period" => period}) do
    data = Plausible.Stats.fetch(site_id, period)
    json(conn, data)
  end
end

Source: [lib/plausible_web/controllers/api/stats_controller.ex](https://github.com/plausible/analytics/blob/master/lib/plausible_web/controllers/api/stats_controller.ex)

Routes are defined in lib/plausible_web/router.ex, mapping /api/v1/* paths to their respective controller actions.

End-to-End Data Flow

The architecture supports unidirectional data flow from user interaction to rendered analytics:

  1. Initial Mount – app.js bootstraps the React tree and mounts <Dashboard />
  2. State Initialization – Context providers establish default values for period selection and real-time flags
  3. User Interaction – Selections in nav-menu/top-bar.tsx update DashboardStateContext, triggering dependent component re-renders
  4. API Request – Feature modules (Sources, Pages, Devices) invoke getStats() from util/api.ts, constructing URLs like /api/v1/stats?site_id=<ID>&period=<RANGE>
  5. Backend Processing – Phoenix routers dispatch to controllers, which execute database queries and serialize results to JSON
  6. UI Update – Components receive JSON responses, update local state via useState or useEffect, and re-render charts and tables with fresh data

This flow ensures that heavy aggregation logic remains server-side while the frontend handles presentation and user interaction states.

Summary

  • Plausible's frontend is a React/TypeScript SPA located in assets/js/dashboard/ with a component-based architecture
  • State management relies on React Context providers (DashboardStateContext, CurrentVisitorsContext, etc.) for global data sharing
  • Backend communication uses a thin fetch wrapper in util/api.ts to call RESTful JSON endpoints served by Elixir Phoenix controllers
  • Data flow moves from context updates to API calls, then to component state updates, ensuring a clear unidirectional pattern
  • Decoupled deployment allows the static frontend to be served separately from the Phoenix backend, which handles all database interactions

Frequently Asked Questions

How does the Plausible frontend communicate with the backend?

The Plausible frontend communicates through a centralized API utility in assets/js/dashboard/util/api.ts that wraps window.fetch to make HTTP requests to Phoenix controller endpoints. These requests include the site ID and authentication credentials, returning JSON data that populates the React component state.

What technology stack powers the Plausible backend API?

The backend is built with Elixir and the Phoenix framework, using PostgreSQL for data storage. Controllers in lib/plausible_web/controllers/api/ handle incoming requests, perform database aggregations, and return JSON responses to the React frontend.

Where is the React application mounted in the Plausible codebase?

The React application mounts in assets/js/app.js, which imports the Dashboard component and renders it into a DOM element with the ID plausible-dashboard. The main dashboard logic and provider wrapping occur in assets/js/dashboard/index.tsx.

Why does Plausible use React Context instead of Redux for state management?

Plausible uses React Context providers for state management to avoid the boilerplate and complexity of Redux while still enabling global state access across deeply nested components. Contexts like GraphIntervalContext and ImportsIncludedProvider efficiently manage specific domain states without additional dependencies.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →