From 99f7ac9bfd1dd19ce616b824dc125c82b66ffadd Mon Sep 17 00:00:00 2001 From: Phil Sarin Date: Wed, 15 Jul 2026 19:14:34 +0000 Subject: [PATCH] docs: add contributor architecture overview Add docs/ARCHITECTURE.md mapping the codebase for contributors: the collection (write) vs. dashboard (read) halves, the API request lifecycle, the multi-backend query layer, the core data model, and a "where do I make a change?" reference table. Link it from the README. Run-on: Niteshift Staging Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 + docs/ARCHITECTURE.md | 173 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/README.md b/README.md index 17dd827bd..292f2bc88 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.is/docs/). +New to the codebase? See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for a +contributor-focused overview of how the project is structured. + --- ## πŸ›  Installing from Source diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..4ecb2c646 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,173 @@ +# Umami Architecture Overview + +Umami is a privacy-focused, self-hosted web analytics platform β€” an alternative to +Google Analytics that collects visitor data **without cookies and without collecting +personal information**. This document is a map of the codebase for contributors: what +the major pieces are, how a request flows through the system, and where to look when +working on a given feature. + +For day-to-day setup and installation, see [`README.md`](../README.md). For +sandbox-specific and prototyping guidance, see [`CLAUDE.md`](../CLAUDE.md). + +## Tech stack + +| Concern | Choice | +| ------------------ | ------------------------------------------------------------- | +| Framework | Next.js 15 (App Router), React 19 | +| Language | TypeScript (ES2022) | +| Database | PostgreSQL via Prisma ORM (optional ClickHouse for analytics) | +| Data fetching (UI) | `@tanstack/react-query` | +| Client state | Zustand stores (`src/store/`) | +| UI components | `@umami/react-zen` component library | +| i18n | `react-intl` via the `useMessages` hook | +| Charts | Chart.js + react-spring | +| Package manager | pnpm | + +## The two halves of the system + +Umami is really two applications sharing one codebase and database. + +### 1. Data collection (write path) + +A lightweight tracker script runs on customer websites and sends a small payload on +each pageview or custom event. The server enriches the payload (device, browser, OS, +geo, referrer) and persists it. + +- **`src/tracker/`** β€” source for the client-side tracking script that sites embed. +- **`src/app/(collect)/`** β€” the public collection endpoints: + - `p/[slug]` β€” pageview/event collection + - `q/[slug]` β€” queue/secondary collection +- **`src/app/api/send/`** β€” the primary event ingestion API route. +- **`src/lib/detect.ts`, `src/lib/ip.ts`** β€” user-agent parsing and IP/geo detection + used to enrich incoming hits. + +Incoming hits are stored as `Session` (one per visitor session) plus `WebsiteEvent` +rows (one per pageview or custom event), with optional `EventData` / `SessionData` +for custom properties. + +### 2. Analytics dashboard (read path) + +The authenticated app where users view reports, manage websites, and configure teams. + +- **`src/app/(main)/`** β€” the authenticated UI. Key sections: + - `websites/` β€” per-site analytics dashboards + - `dashboard/` & `boards/` β€” cross-website overview boards + - `reports/` β€” saved custom reports (funnels, retention, attribution, etc.) + - `links/` β€” short-link tracking + - `pixels/` β€” tracking-pixel management + - `teams/`, `settings/`, `admin/` β€” administration +- **`src/app/share/`** β€” public, read-only shared report pages. + +## Request lifecycle (API routes) + +API routes live under `src/app/api/**/route.ts` and follow a consistent pattern: + +```ts +export async function GET(request: Request) { + const { query, auth, error } = await parseRequest(request, schema); // parse + Zod-validate + if (error) return error(); // early-return on validation/auth failure + // ...authorization + business logic... + return json(data); // response helpers from @/lib/response +} +``` + +1. **Parse & validate** β€” `parseRequest()` (`src/lib/request.ts`) parses the request + and validates it against a Zod schema. +2. **Authenticate** β€” `checkAuth()` (`src/lib/auth.ts`); can be bypassed with + `skipAuth: true` for public collection endpoints. +3. **Authorize** β€” permission checks in `src/permissions/`. +4. **Query** β€” via the query layer (below). +5. **Respond** β€” helpers in `src/lib/response.ts` (`json`, `unauthorized`, etc.). + +## Data & query layer + +The query layer abstracts over multiple database backends through `runQuery()` in +[`src/lib/db.ts`](../src/lib/db.ts): + +```ts +return runQuery({ + [PRISMA]: () => prismaQuery(), + [CLICKHOUSE]: () => clickhouseQuery(), +}); +``` + +- **`src/queries/prisma/`** β€” entity CRUD via Prisma (website, user, team, report, + link, pixel, segment, …). +- **`src/queries/sql/`** β€” hand-written SQL for analytics aggregations, grouped by + concern: `events/`, `pageviews/`, `sessions/`, `reports/`, plus realtime and stats + helpers. +- **Generated Prisma client** lives in `src/generated/prisma/` (non-default location). + **Never edit generated files by hand** β€” regenerate with `pnpm build-db-client` + after changing `prisma/schema.prisma`. + +### Core data model (`prisma/schema.prisma`) + +| Model | Purpose | +| ----------------------------- | ---------------------------------------------- | +| `User`, `Team`, `TeamUser` | Accounts, organizations, and membership | +| `Website` | A tracked site | +| `Session`, `SessionData` | A visitor session and its custom properties | +| `WebsiteEvent`, `EventData` | A pageview/custom event and its properties | +| `Report`, `Segment` | Saved reports and reusable audience segments | +| `Revenue` | Revenue attribution data | +| `Link`, `Pixel` | Short-link tracking and tracking pixels | + +## Frontend architecture + +- **Component library first** β€” reach for `@umami/react-zen` primitives (`Button`, + `Form`, `Row`, `Column`, `Grid`, `Modal`, …) before writing custom CSS. +- **Shared components** β€” `src/components/common/` (`PageBody`, `PageHeader`, `Panel`, + `DataGrid`, `Empty`, …). Feature-specific groups: `charts/`, `metrics/`, `boards/`, + `input/`. +- **Data fetching** β€” prefer the pre-built query hooks in + `src/components/hooks/queries/` (e.g. `useWebsitesQuery`, `useReportsQuery`). Fall + back to `useApi()` + React Query for one-off calls. +- **Global state** β€” Zustand stores in `src/store/` (`app`, `dashboard`, `websites`, + `cache`, `version`). +- **i18n** β€” all user-facing strings go through `useMessages()`; strings are defined + in `src/components/messages.ts` and compiled to `public/intl/messages/`. + +## Directory quick reference + +``` +src/ +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ (main)/ Authenticated dashboard UI +β”‚ β”œβ”€β”€ (collect)/ Public data-collection endpoints (p/, q/) +β”‚ β”œβ”€β”€ api/ REST API routes +β”‚ β”œβ”€β”€ login/ sso/ Authentication pages +β”‚ └── share/ Public shared reports +β”œβ”€β”€ tracker/ Client-side tracking script source +β”œβ”€β”€ lib/ Core utilities (db, auth, request/response, date, detect, ip) +β”œβ”€β”€ queries/ DB query layer (prisma/ + raw sql/) +β”œβ”€β”€ permissions/ Authorization checks +β”œβ”€β”€ components/ React UI (common, input, charts, metrics, boards, hooks) +β”œβ”€β”€ store/ Zustand state stores +β”œβ”€β”€ config/ App/config constants and the Niteshift design manifest +└── generated/prisma/ Auto-generated Prisma client (do not edit) +``` + +## Where do I make a change? + +| I want to… | Start here | +| -------------------------------- | -------------------------------------------------- | +| Add a dashboard page | `src/app/(main)//page.tsx` | +| Add an API endpoint | `src/app/api//route.ts` | +| Add a reusable component | `src/components/common/` | +| Add a data hook | `src/components/hooks/queries/` | +| Change an analytics aggregation | `src/queries/sql/` | +| Change an entity CRUD query | `src/queries/prisma/` | +| Change the DB schema | `prisma/schema.prisma` β†’ `pnpm build-db-client` | +| Change what the tracker collects | `src/tracker/` β†’ `pnpm build-tracker` | + +## Common commands + +```bash +pnpm dev # Dev server (port 3001, Turbo) +pnpm test # Jest unit tests +pnpm lint # ESLint (also runs on pre-commit) +pnpm build-db-client # Regenerate the Prisma client after schema changes +pnpm update-db # Apply new migrations +pnpm build-tracker # Rebuild the tracking script +pnpm build-geo # Rebuild the GeoIP database +```