# Plausible Analytics React Frontend Architecture and Backend Communication

> Explore Plausible Analytics' React frontend architecture and backend communication. Discover its React/TypeScript SPA, Elixir Phoenix backend, RESTful JSON API, context providers, and fetch utility layer.

- Repository: [Plausible Analytics/analytics](https://github.com/plausible/analytics)
- Tags: architecture
- Published: 2026-05-19

---

**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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`.

```typescript
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/main/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`](https://github.com/plausible/analytics/blob/main/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.

```tsx
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/main/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`](https://github.com/plausible/analytics/blob/main/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.

```typescript
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/main/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.

```elixir
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/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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`](https://github.com/plausible/analytics/blob/main/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.