apps/
monitor-app # Next.js dashboard & API
anomaly-worker # Sidecar service for anomaly detection (new)
packages/
client-sdk # Browser SDK for CWV collection
cwv-monitor-contracts # Shared ingest payload schemas
flowchart LR
subgraph Client["Instrumented Next.js app"]
SDK["next-cwv-monitor<br>(web vitals + custom events)"]
end
subgraph Monitor["Monitor App (Next.js 16)"]
Ingest["POST /api/ingest<br>schema + rate limit"]
Dashboard["Dashboard UI + auth"]
end
subgraph Worker["Anomaly Worker"]
Poller["Poller (node-cron)"]
Notifier["Slack/Teams Notifier"]
end
subgraph CH["ClickHouse"]
Raw["cwv_events & custom_events<br>MergeTree, TTL 90d"]
Agg["cwv_daily_aggregates<br>AggregatingMergeTree, 365d"]
Anom["v_cwv_anomalies (View)<br>Z-Score logic"]
end
SDK -->|"batched payload"| Ingest
Ingest -->|"validated & enriched"| Raw
Raw -->|"mv_cwv_daily_aggregates"| Agg
Dashboard -->|"analytics queries"| Agg
Dashboard -->|"drill-down"| Raw
Poller -->|"polls every hour"| Anom
Poller -->|"fetches project info"| Raw
Poller -->|"marks as notified"| Raw
Poller -->|"sends notifications"| Notifier
sequenceDiagram
participant Browser
participant SDK
participant API as /api/ingest
participant CH as ClickHouse
Browser->>SDK: Web Vitals + custom events occur
SDK-->>SDK: Queue & batch (size/time/unload triggers)
SDK->>API: POST batched payload
API-->>API: Validate (arktype schema)
API-->>API: Rate limit by IP (if enabled)
API->>CH: Insert rows into cwv_events / custom_events
CH-->>CH: Materialized view updates daily aggregates
API-->>SDK: 204 No Content (or error)
The main dashboard for viewing Core Web Vitals, managing projects, and administering users.
- Stack: Next.js 16 App Router, Tailwind CSS, shadcn/ui
- Auth: Better Auth with role-based access (admin, user)
- Ingest API: Receives batched events from SDK, validates via arktype schemas, rate-limits by IP, and persists to ClickHouse
- Deployment: Docker container via
monitor-app.prod.Dockerfile
Lightweight browser SDK that collects CWV metrics (LCP, INP, CLS, TTFB, FCP), $page_view events, and custom business events.
- Stack: TypeScript, web-vitals, React hooks
- Entrypoints:
next-cwv-monitor/app-routerandnext-cwv-monitor/pages-router - Privacy: In-memory session ID per page view — no cookies or localStorage
- Batching: Flushes on 50ms idle or page unload/visibility change (sendBeacon)
Shared TypeScript schemas for ingest payloads. Imported by both SDK and monitor app to prevent drift.
A lightweight sidecar service that periodically polls ClickHouse for statistical anomalies (z_score > 3) and sends notifications.
- Stack: Node.js, node-cron, ClickHouse client, pino
- Polling: Scheduled every hour to detect new regressions
- State: Tracks notified anomalies in
processed_anomaliestable to prevent duplicates - Notifications: Supports Slack and Microsoft Teams webhooks
- Deployment: Dedicated sidecar container (see
docker/anomaly-worker.Dockerfile)
ClickHouse — High-performance columnar database for analytics.
Full schema details:
apps/monitor-app/clickhouse/SCHEMA.md
Design choices:
project_idin every table for multi-tenancy- Append-only MergeTree for fast writes
quantilesState()for efficient percentile queries (p50–p99)- TTL-based retention for automatic cleanup
LowCardinalityfor enum-like columns
| Mode | Command | Description |
|---|---|---|
| Development | pnpm docker:dev |
Hot reload, seeded demo data |
| Production | Setup wizard | Guided setup with SSL option |
The setup wizard downloads Docker Compose files, configures secrets automatically, and optionally sets up SSL via Caddy.
See DEPLOYMENT.md for full configuration options and environment variables.
- Auth: Better Auth with email/password; role-based access control
- Rate limiting: IP-based on ingest endpoint
- Sessions: Stored in ClickHouse with TTL expiration
- SDK privacy: Ephemeral session IDs only — no PII collected