A production-ready web performance monitoring application built with Next.js 15, featuring automated PageSpeed Insights audits, scheduled monitoring, and comprehensive performance analytics.
π For detailed architecture and implementation documentation, see ARCHITECTURE.md
π§ͺ For testing setup, tools, and conventions, see TESTING.md
ποΈ For database seed and cleanup scripts, see DATABASE_SCRIPTS.md
π For the Run History page and its seed scripts, see docs/RUN-HISTORY.md
- π Site Monitoring: Track multiple websites with customizable monitoring schedules
- π Performance Metrics: Core Web Vitals (LCP, INP, CLS, FCP, TTFB), Lighthouse scores, and environment metadata (browser user agent, benchmark index, form factor)
- πΈ Visual Snapshots: Capture and store page screenshots with each audit
- π Trend Analysis: Visualize performance over time with interactive charts
- π Automated Audits: Background worker with cron scheduler for periodic testing
- π― Manual Runs: On-demand performance audits with rate limiting
- π Run History: Dedicated history page with site/monitor selector, 7d/14d/30d date range, Scores and Core Web Vitals chart tabs, and a full-detail run table
- π Run Comparison: Side-by-side comparison of metrics, audits, and screenshots
- π€ AI Analysis: GPT-4o-mini powered narrative summaries with prioritized action items for each run
- π» CLI: Terminal client for triggering runs, inspecting results, and managing sites without leaving the terminal
- π Authentication: Google, GitHub, and email magic link authentication via NextAuth
- β‘ Queue System: BullMQ-powered job processing with retry logic
- π§Ή Auto-Cleanup: Automatic screenshot TTL policy to manage database size
- π¨ Modern UI: Beautiful interface built with shadcn/ui and Tailwind CSS
- Next.js 15 (App Router) - React framework
- TypeScript - Type safety
- Tailwind CSS - Styling
- shadcn/ui - UI components
- PostgreSQL - Primary database
- Prisma - Type-safe ORM
- NextAuth v5 - Google, GitHub, and email magic link authentication
- Prisma Adapter - Database session storage
- BullMQ - Job queue management
- Redis - Queue backing store
- node-cron - Scheduled job execution
- OpenAI GPT-4o-mini - AI-generated run summaries (via Vercel AI SDK)
- Google PageSpeed Insights API - Performance audits
- Zod - Runtime validation
- Recharts - Data visualization
The application follows a Next.js monorepo structure with a separate background worker process:
βββββββββββββββββββ
β Next.js App β
β (UI + API) β
ββββββββββ¬βββββββββ
β
ββββββ΄βββββ
βββββββββββΌβββββββββββ
β β β
βββββΌββββ ββββΌββββ ββββββΌβββββ
βPrisma β βRedis β β BullMQ β
β ORM β β(Rate β β Queue β
βββββ¬ββββ βLimit)β ββββββ¬βββββ
β ββββββββ β
βββββΌββββββββββ ββββββΌββββββ
β PostgreSQL β β Worker β
β Database β β Process β
βββββββββββββββ ββββββ¬ββββββ
β
ββββββΌβββββ
β PSI β
β API β
βββββββββββ
π For comprehensive architecture documentation, including:
- Detailed component diagrams
- Data flow explanations
- Backend patterns and best practices
- Frontend architecture details
- Database schema and relationships
- Background job processing
- Authentication flow
See ARCHITECTURE.md
- Node.js 20.x or higher
- Docker and Docker Compose
- pnpm (recommended) or npm
- Google PageSpeed Insights API key (Get one here)
git clone <repository-url>
cd side
pnpm installCopy the example environment file and configure your variables:
cp .env.example .envEdit .env with your configuration:
# Database
DATABASE_URL="postgresql://perflab:perflab@localhost:5432/perflab?schema=public"
# NextAuth (generate with: openssl rand -base64 32)
NEXTAUTH_SECRET="your-secret-here"
NEXTAUTH_URL="http://localhost:3000"
# OAuth Providers
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
# Email (SMTP for magic links β optional if only using OAuth)
EMAIL_SERVER="smtp://user:password@smtp.example.com:587"
EMAIL_FROM="noreply@example.com"
# Redis
REDIS_HOST="localhost"
REDIS_PORT="6379"
REDIS_PASSWORD=""
# PageSpeed Insights API
PAGESPEED_API_KEY="your-api-key-here"
# Scheduler (generate with: openssl rand -base64 32)
SCHEDULER_SECRET="your-scheduler-secret-here"
# Rate limiting
RATE_LIMIT_RUNS_PER_DAY="100"
# Screenshot cleanup (TTL in days)
SCREENSHOT_TTL_DAYS="30"
# OpenAI (for AI run summaries)
OPENAI_API_KEY="sk-..."
# Environment
NODE_ENV="development"Start PostgreSQL and Redis using Docker Compose:
docker-compose up -dVerify services are running:
docker-compose psRun Prisma migrations to set up the database schema:
pnpm prisma generate
pnpm prisma db pushYou have two options:
Option A: Run everything together (recommended for development)
pnpm dev:allThis starts both the Next.js app and the worker process concurrently.
Option B: Run separately
Terminal 1 - Next.js app:
pnpm devTerminal 2 - Worker process:
pnpm dev:workerOpen http://localhost:3000 in your browser.
You'll be redirected to the sign-in page. Enter your email to receive a magic link.
/
βββ cli/ # Terminal CLI (pnpm workspace package)
β βββ src/
β β βββ index.ts # Command entry point
β β βββ client.ts # HTTP client (Bearer auth)
β β βββ config.ts # Persistent config (~/.config/side-cli/)
β β βββ format.ts # Output formatters
β β βββ ui.tsx # ink terminal components
β β βββ commands/ # auth, sites, monitors, run
β βββ README.md # Full CLI documentation
βββ prisma/
β βββ schema.prisma # Database schema
βββ src/
β βββ app/
β β βββ api/ # API routes
β β β βββ auth/ # NextAuth endpoints
β β β βββ cli/ # CLI device-flow auth (login/poll)
β β β βββ keys/ # API key management
β β β βββ sites/ # Site CRUD
β β β βββ monitors/ # Monitor CRUD + run trigger
β β β βββ runs/ # Run details + comparison
β β β βββ scheduler/ # Scheduler trigger endpoint
β β βββ (app)/ # Sidebar layout group
β β β βββ dashboard/ # Main dashboard page
β β β βββ sites/[id]/ # Site detail page
β β β βββ runs/[id]/ # Run detail + comparison pages
β β β βββ alerts/ # Regression alerts
β β β βββ settings/ # API key management UI
β β βββ cli/authorize/ # Browser authorization page (device flow)
β β βββ auth/ # Auth pages (signin, verify)
β βββ components/
β β βββ ui/ # shadcn/ui components
β β βββ site-form.tsx # Site creation form
β β βββ monitor-form.tsx # Monitor creation form
β β βββ run-button.tsx # Manual run trigger
β β βββ score-badge.tsx # Score visualization
β β βββ metrics-chart.tsx # Performance charts
β βββ lib/
β β βββ auth.ts # NextAuth configuration
β β βββ prisma.ts # Prisma client
β β βββ redis.ts # Redis client
β β βββ queue.ts # BullMQ queue setup
β β βββ api-key-auth.ts # API key generation and hashing
β β βββ resolve-user.ts # Bearer token + session resolution
β β βββ psi-parser.ts # PageSpeed Insights parser
β β βββ regression/ # Regression detection engine
β βββ worker/
β β βββ index.ts # Worker entry point
β β βββ processor.ts # Job processing logic
β β βββ scheduler.ts # Cron scheduler
β βββ types/
β β βββ api.ts # Shared API response types (used by CLI too)
β β βββ next-auth.d.ts # NextAuth type extensions
β βββ env.js # Environment validation
βββ docker-compose.yml # Postgres + Redis
βββ .env.example # Environment template
βββ package.json # Dependencies and scripts
βββ README.md # This file
βββ docs/ # Documentation
βββ ARCHITECTURE.md # Detailed architecture docs
βββ DATABASE_SCRIPTS.md # Seed and cleanup scripts
βββ TESTING.md # Testing guide and conventions
π For detailed explanations of each component and module, see ARCHITECTURE.md
- Sign in with your email
- Click "Create Site" on the dashboard
- Enter site name and URL
- Navigate to the site detail page
- Click "Create Monitor" to set up automated auditing
- Configure cadence (how often to run) and strategy (mobile/desktop)
- Navigate to a site detail page
- Find the monitor you want to test
- Click "Run Now"
- The run will be queued and processed by the worker
- Refresh the page to see results
- Dashboard: Overview of all sites with latest scores
- Site Detail: Timeline charts and run history per monitor
- Run History: Cross-monitor history with date-range filtering and CWV chart tab
- Run Detail: Complete metrics, scores, and audits
- Run Comparison: Side-by-side comparison of two runs
The worker's built-in cron scheduler runs every minute and:
- Finds all active monitors where
nextRunAt <= now - Creates a run and enqueues a job
- Updates
nextRunAtbased oncadenceMinutes - Processes the job via BullMQ worker
GET /api/sites- List user sitesPOST /api/sites- Create siteGET /api/sites/[id]- Get site detailsPUT /api/sites/[id]- Update siteDELETE /api/sites/[id]- Delete site
GET /api/monitors?siteId=X- List monitorsPOST /api/monitors- Create monitorPUT /api/monitors/[id]- Update monitorDELETE /api/monitors/[id]- Delete monitorPOST /api/monitors/[id]/run- Trigger manual run (rate limited)
GET /api/runs?monitorId=X- List runsGET /api/runs/[id]- Get run detailsGET /api/runs/[id]/compare/[id2]- Compare two runsPOST /api/runs/[id]/ai-summary- Generate (or regenerate) an AI summary for a run (streams response)
POST /api/scheduler/tick- Trigger scheduler (requiresx-scheduler-secretheader)
POST /api/cli/login- Start device flow; returns{ loginCode, authorizeUrl }GET /api/cli/login?code=X- Poll for authorization status; returns raw API key once on success
GET /api/keys- List API keys for the authenticated userPOST /api/keys- Create a named API key (returns raw key once)DELETE /api/keys/[id]- Revoke a key
Manual runs are rate limited per user per day:
- Default: 100 runs/day
- Configurable via
RATE_LIMIT_RUNS_PER_DAY - Uses Redis for distributed tracking
- Resets at midnight
Screenshots from PageSpeed Insights are automatically captured and stored:
- Storage: Base64-encoded JPEG in PostgreSQL
- Display: Click-to-zoom thumbnails on run detail pages
- Comparison: Side-by-side screenshots when comparing runs
- TTL Policy: Automatic cleanup of screenshots older than 30 days (configurable via
SCREENSHOT_TTL_DAYS) - Cleanup Schedule: Runs daily at 3 AM via the worker's cron scheduler
- Manual Cleanup: Run
pnpm cleanup:screenshots [days]to manually clean up screenshots
# Build Next.js app
pnpm build
# Build worker
pnpm build:worker
# Start Next.js
pnpm start
# Start worker (separate process/container)
pnpm start:workerEnsure all production environment variables are set:
- Use strong secrets for
NEXTAUTH_SECRETandSCHEDULER_SECRET - Configure production SMTP server for
EMAIL_SERVER - Set
NEXTAUTH_URLto your production domain - Use managed PostgreSQL and Redis for production
- Secure
PAGESPEED_API_KEY
For production, deploy the application and worker as separate containers:
- Next.js App: Handles HTTP requests
- Worker Process: Processes queue jobs and runs scheduler
- PostgreSQL: Database
- Redis: Queue and rate limiting
- Horizontal: Run multiple worker processes for parallel job processing
- Queue Concurrency: Adjust BullMQ concurrency in
src/worker/index.ts - Database: Use connection pooling (Prisma supports this)
- Redis: Use Redis Cluster for high availability
The cli/ directory is a pnpm workspace package (@side/cli) that provides a terminal interface to the web app's API. It has no direct database access β all operations go through the same API routes used by the web UI.
# Authenticate (opens browser, saves API key to ~/.config/side-cli/)
side auth --url https://yourapp.com
# List sites and their monitors
side sites list
# Create a site (--monitor also creates a default mobile monitor)
side sites add https://example.com --name "Example" --monitor
# Trigger an on-demand PSI run and stream results
side run <monitorId>Setup:
pnpm cli:build # compile TypeScript
node cli/dist/cli/src/index.js auth --url http://localhost:3000
# or link globally:
cd cli && pnpm link --global
side authAll CLI scripts (run from repo root):
| Script | Description |
|---|---|
pnpm cli:build |
Compile TypeScript to cli/dist/ |
pnpm cli:dev |
Watch mode |
pnpm cli:lint |
ESLint on cli/src/ |
pnpm cli:test |
Vitest unit tests |
pnpm cli:test:watch |
Vitest watch mode |
For full CLI documentation including all commands, auth flow, CI/CD usage, and shared types contract, see cli/README.md.
For seeding test data and cleaning up the database during development, see DATABASE_SCRIPTS.md.
Quick reference:
# Seed regression alerts
pnpm seed:regressions your-email@example.com
# Seed Run History test data (gradual decline / improvement)
pnpm seed:decline your-email@example.com
pnpm seed:improvement your-email@example.com
# Seed Activity feed events (50 events across all types, last 30 days)
pnpm seed:activity your-email@example.com
# Clean database (preserves users/sessions)
pnpm seed:clean
# Fresh start: clean + seed
pnpm seed:clean && pnpm seed:regressions your-email@example.comAfter modifying prisma/schema.prisma:
pnpm prisma generate
pnpm prisma db pushFor production migrations:
pnpm prisma migrate dev --name <migration-name>npx shadcn@latest add <component-name># Run unit / integration / component tests (web app)
pnpm test
# Watch mode
pnpm test:watch
# Coverage report
pnpm test:coverage
# E2E tests (Playwright)
pnpm test:e2e
# CLI unit tests
pnpm cli:test
pnpm cli:test:watchFor full details on the testing strategy, tools, and conventions, see TESTING.md.
# Web app (cli/ is excluded)
pnpm tsc --noEmit# Web app (cli/ is excluded)
pnpm lint
# CLI
pnpm cli:lintThis project uses PostHog for product analytics. The client is initialized in src/instrumentation-client.ts, which Next.js 15.3+ loads automatically β no manual imports required.
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_POSTHOG_KEY |
Production | Your PostHog project API key |
NEXT_PUBLIC_POSTHOG_HOST |
Production | PostHog ingestion host (e.g. https://us.i.posthog.com) |
NEXT_PUBLIC_POSTHOG_ENABLED |
Dev only | Set to "true" to enable PostHog in local development |
PostHog is always enabled in production. In other environments it only runs when NEXT_PUBLIC_POSTHOG_ENABLED=true is set in .env.local.
All event names must be lowercase with underscores (snake_case):
// good
posthog.capture("site_created")
posthog.capture("manual_run_triggered")
// bad β don't use these patterns
posthog.capture("siteCreated")
posthog.capture("Site Created")
posthog.capture("SITE_CREATED")Prefer a const object with as const over a TypeScript enum. Enums compile to runtime JavaScript; as const is zero-cost and lets you derive a union type automatically:
// src/lib/analytics-events.ts
export const AnalyticsEvent = {
site_created: "site_created",
site_deleted: "site_deleted",
monitor_created: "monitor_created",
manual_run_triggered: "manual_run_triggered",
alert_viewed: "alert_viewed",
} as const;
export type AnalyticsEventName = typeof AnalyticsEvent[keyof typeof AnalyticsEvent];Then import and use it anywhere:
import posthog from "posthog-js";
import { AnalyticsEvent } from "@/lib/analytics-events";
posthog.capture(AnalyticsEvent.site_created, { url: site.url });- PostHog Next.js integration guide
- posthog.capture() API reference
- Event properties best practices
- Feature flags
- Session replay
This project uses Sentry for error monitoring, performance tracing, session replay, and cron job health tracking across the Next.js app and the background worker process.
π For detailed architecture and implementation documentation, see docs/sentry-integration.md
- Errors β unhandled exceptions, promise rejections, API route failures, worker job crashes
- Traces β server request timing, client navigation spans, Web Vitals (LCP, CLS, FCP, TTFB)
- Session Replay β video-like reproductions of user sessions when errors occur
- Cron health β missed, failed, or timed-out scheduled jobs in the worker
| Variable | Required | Description |
|---|---|---|
SENTRY_DSN |
Optional | Server-side DSN for the Next.js server and worker process |
NEXT_PUBLIC_SENTRY_DSN |
Optional | Client-side DSN (safe to expose in browser bundle) |
SENTRY_AUTH_TOKEN |
CI only | Auth token for source map uploads during production builds |
SENTRY_ORG |
CI only | Sentry organization slug |
SENTRY_PROJECT |
CI only | Sentry project slug |
Sentry is optional β the app runs without it when DSN variables are not set. The SDK silently no-ops.
To test Sentry locally:
- Create a free account at sentry.io and create a Next.js project
- Copy the DSN from Settings β Projects β Client Keys (DSN)
- Add to your
.env:
SENTRY_DSN=https://your-key@o0.ingest.sentry.io/0
NEXT_PUBLIC_SENTRY_DSN=https://your-key@o0.ingest.sentry.io/0- Restart the dev server (
pnpm dev:all)
To see SDK debug logs in the console, temporarily add debug: true to any Sentry.init() call.
Throw a test error in a client component or API route:
// In any client component β click a button that runs this:
throw new Error("Sentry test error β delete me");
// Or in any API route:
import * as Sentry from "@sentry/nextjs";
Sentry.captureException(new Error("Sentry test error β delete me"));Check sentry.io/issues/ β the error should appear within ~30 seconds.
- Sentry Next.js integration docs
- Session Replay privacy configuration
- Cron Monitoring
- Source map setup
- Go to the Google Cloud Console
- Create a new project (or select an existing one)
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Select Web application as the application type
- Add
http://localhost:3000to Authorized JavaScript origins - Add
http://localhost:3000/api/auth/callback/googleto Authorized redirect URIs - Copy the Client ID and Client Secret into your
.env
- Go to GitHub Developer Settings
- Click New OAuth App
- Set Homepage URL to
http://localhost:3000 - Set Authorization callback URL to
http://localhost:3000/api/auth/callback/github - Click Register application
- Copy the Client ID and generate a Client Secret, then add both to your
.env
For production, replace
http://localhost:3000with your production URL in both providers.
- Check Redis connection:
docker-compose ps - Check worker logs:
pnpm dev:worker - Verify environment variables in
.env
- Ensure PostgreSQL is running:
docker-compose ps - Verify
DATABASE_URLin.env - Check Prisma schema:
pnpm prisma studio
- Verify API key is valid
- Check API quotas and limits
- Review worker logs for error messages
- Verify SMTP configuration in
.env - Test SMTP credentials
- Check email provider logs
We welcome contributions! Before getting started:
- Read the documentation: Check out ARCHITECTURE.md to understand the codebase
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes: Follow the code style and patterns in the codebase
- Test your changes: Run
pnpm testand ensure all tests pass - Run linting:
pnpm lint - Submit a pull request: Include a clear description of your changes
- Review ARCHITECTURE.md for detailed implementation patterns
- Start with small changes to get familiar with the codebase
- Ask questions by opening an issue
MIT
For issues and questions, please open a GitHub issue.