Primeira Liga Stats
A self-hosted, real-time football statistics dashboard for the Portuguese Primeira Liga (and optionally UEFA Euro). Built with Next.js 15, PostgreSQL 17, and the API-Football data provider. The entire stack runs as Docker containers — one command to deploy.
Table of Contents
- Overview
- Tech Stack
- Architecture
- Project Structure
- Prerequisites
- Quick Start
- Configuration
- Environment Variables
- Features
- Pages & Routes
- API Endpoints
- Database
- Admin Panel
- Performance & Caching
- Docker Reference
- Data Persistence & Backups
- Changing the Port
- API Usage & Rate Limits
- Troubleshooting
- Development (Local, without Docker)
- System Requirements
Overview
Primeira Liga Stats is a production-ready web dashboard that displays live and historical football data. It polls API-Football v3 for match data and presents it in a clean, auto-refreshing UI. All configuration — including the API key, color theme, visible sections, and refresh interval — can be changed at runtime through the built-in Admin Panel without restarting the application.
Tech Stack
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js | 15.1.0 |
| Language | TypeScript | 5.7 |
| Runtime | Node.js | 22 LTS (Alpine) |
| Database | PostgreSQL | 17 Alpine |
| ORM | Prisma | 5.22 |
| UI Components | Radix UI | Latest |
| Styling | Tailwind CSS | 3.4 |
| Icons | Lucide React | 0.460 |
| Container Runtime | Docker / Docker Compose | — |
| Data Source | API-Football v3 | — |
Architecture
┌─────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────────┐ ┌─────────────┐ │
│ │ app container │ │ db container│ │
│ │ (Next.js 15) │◄─┤ (Postgres17)│ │
│ │ Node 22 Alpine │ │ │ │
│ │ Port 3000 │ │ Port 5432 │ │
│ └────────┬─────────┘ └─────────────┘ │
│ │ │
└───────────┼─────────────────────────────┘
│
▼ HTTP
http://localhost:3000
▲ HTTPS
│
API-Football v3 (external)
v3.football.api-sports.io
The Next.js app acts as a backend-for-frontend: all API-Football requests are proxied through Next.js API routes. The browser never sees the API key. Settings and request logs are stored in PostgreSQL via Prisma.
Project Structure
primeira_liga_stats/
├── Dockerfile # Multi-stage Docker build (Node 22)
├── docker-compose.yml # Orchestrates app + db services
├── .dockerignore # Excludes node_modules, .next, etc.
├── start.sh # Container entrypoint: migrate + start
├── init-db/
│ └── 01-init.sql # Auto-runs on first DB container start
└── nextjs_space/
├── package.json.docker # Dependencies (used by Dockerfile)
├── next.config.js # Next.js config (standalone, compression)
├── tailwind.config.ts # Tailwind theme + dark mode config
├── tsconfig.json # TypeScript compiler config
├── postcss.config.js # PostCSS config
├── components.json # shadcn/ui component registry
├── prisma/
│ └── schema.prisma # Data models: Settings, ApiRequestLog
├── lib/
│ ├── db.ts # Prisma client singleton (with dev logging)
│ ├── settings.ts # Settings fetch + 30s in-memory cache
│ ├── settings-context.tsx # React context for settings
│ ├── api-logger.ts # Non-blocking request logger + pruning
│ ├── types.ts # Shared TypeScript types
│ └── utils.ts # Utility helpers (cn, etc.)
├── app/
│ ├── layout.tsx # Root layout (Inter font, metadata)
│ ├── page.tsx # Main dashboard page (client component)
│ ├── globals.css # Global CSS + Tailwind directives
│ ├── theme-provider.tsx # next-themes provider
│ ├── _components/ # Page-level UI components
│ │ ├── live-matches.tsx # Live score cards with auto-refresh
│ │ ├── standings.tsx # Full league table
│ │ ├── top-scorers.tsx # Top scorers leaderboard
│ │ ├── recent-results.tsx # Last 10 match results
│ │ ├── upcoming-fixtures.tsx # Next 10 fixtures
│ │ └── countdown-timer.tsx # Refresh countdown in header
│ ├── admin/
│ │ ├── layout.tsx # Admin layout wrapper
│ │ ├── page.tsx # Admin dashboard
│ │ └── _components/
│ │ └── api-stats-chart.tsx # Request volume chart
│ └── api/
│ ├── fixtures/route.ts # GET /api/fixtures?type=live|last|next
│ ├── standings/route.ts # GET /api/standings
│ ├── topscorers/route.ts # GET /api/topscorers
│ ├── settings/route.ts # GET /api/settings (public, no API key)
│ └── admin/
│ ├── settings/route.ts # GET|PUT /api/admin/settings
│ └── stats/route.ts # GET /api/admin/stats
├── components/
│ ├── theme-provider.tsx
│ └── ui/ # Radix-based shadcn/ui components
└── public/
├── favicon.svg
└── og-image.png
Prerequisites
- Docker Engine 20.10 or later
- Docker Compose v2.0 or later (
docker composenotdocker-compose) - At least 1 GB RAM available to Docker
- At least 2 GB free disk space
- An API-Football API key (free tier available at api-football.com)
Quick Start
# 1. Clone or unzip the project
cd primeira_liga_stats
# 2. Start everything
docker compose up -d
# 3. Open the app (give it ~30 seconds on first run for the build)
open http://localhost:3000
The Admin Panel is at http://localhost:3000/admin.
That's it. No other setup is needed. The API key is pre-configured in docker-compose.yml.
Configuration
All configuration lives in two places:
1. docker-compose.yml — environment variables (startup time)
Edit this file to change the API key, port, or secrets before starting the containers. Requires a container restart to take effect.
2. Admin Panel at /admin — runtime settings
Change site name, colors, visible sections, refresh interval, API key, and league without restarting. Settings are persisted to the database and take effect immediately.
Environment Variables
These are set in the app service of docker-compose.yml:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql://primeiraliga:primeiraliga2024@db:5432/primeiraliga |
Full Postgres connection string. Includes pool settings. |
API_FOOTBALL_API_KEY |
(pre-configured) | Your API-Football v3 key. Can also be set via Admin Panel. |
NEXTAUTH_URL |
http://localhost:3000 |
Public URL of the app. Change this when deploying to a domain. |
NEXTAUTH_SECRET |
(pre-configured) | Secret used to sign sessions. Change this in production. |
NODE_ENV |
production |
Set automatically. Do not change. |
Security note: Before deploying publicly, generate a new
NEXTAUTH_SECRETwithopenssl rand -base64 32and updatedocker-compose.yml.
Features
Main Dashboard (/)
- Live Matches — real-time scores with elapsed time indicator; auto-refreshes every 30 seconds when there are live games
- League Standings — full table with position, team logo, played/won/drawn/lost, goal difference, and points
- Top Scorers — ranked list of top goal scorers with player photo, team, and statistics
- Recent Results — last 10 completed match results
- Upcoming Fixtures — next 10 scheduled matches with date and time
- Countdown Timer — header widget showing time until next data refresh
- Configurable theme — primary and secondary colors applied across gradients and accents
Admin Panel (/admin)
- Toggle each dashboard section on/off
- Change site name and brand colors
- Switch between Primeira Liga (League ID 94, season 2024) and UEFA Euro 2024 (League ID 4)
- Update the API-Football API key at runtime
- View API request statistics: total requests, today's requests, average response time, success rate, and an hourly request volume chart
Pages & Routes
| Route | Type | Description |
|---|---|---|
/ |
Client page | Main stats dashboard |
/admin |
Client page | Admin configuration panel |
API Endpoints
All routes are internal — they proxy requests to API-Football and return JSON. The browser never sends the API key directly.
GET /api/fixtures?type=<live|last|next>
Fetches fixtures from API-Football.
type param |
Data returned | Revalidation |
|---|---|---|
live |
Currently live matches | 30 seconds |
last |
Last 10 completed results | 5 minutes |
next |
Next 10 upcoming fixtures | 5 minutes |
Responses include Cache-Control: public, s-maxage=N, stale-while-revalidate=2N headers.
GET /api/standings
Returns the full league standings table. Cached for 5 minutes.
GET /api/topscorers
Returns the top scorers list. Cached for 5 minutes.
GET /api/settings
Returns public settings (no API key). Used by the frontend to load theme, visibility flags, and refresh interval.
GET /api/admin/settings
Returns full settings including the API key. Admin use only.
PUT /api/admin/settings
Updates settings. Accepts a JSON body with any subset of the settings fields. Automatically busts the server-side settings cache.
GET /api/admin/stats
Returns API request analytics: total count, today's count, per-endpoint breakdown, hourly chart data (last 24h), average response time, and success rate. All database queries run in parallel via Promise.all.
Database
Engine
PostgreSQL 17 (Alpine), running in its own container (primeira_liga_db).
Connection
| Property | Value |
|---|---|
| Host (from app container) | db |
| Port | 5432 |
| Database | primeiraliga |
| User | primeiraliga |
| Password | primeiraliga2024 |
Schema
Managed by Prisma. There are two tables:
Settings — singleton row (id = 'main') storing all runtime configuration.
| Column | Type | Default |
|---|---|---|
id |
TEXT (PK) | 'main' |
apiKey |
TEXT? | null |
selectedLeague |
TEXT | 'primeira_liga' |
leagueId |
INT | 94 |
season |
INT | 2024 |
siteName |
TEXT | 'Primeira Liga Stats' |
primaryColor |
TEXT | '#E42518' |
secondaryColor |
TEXT | '#006600' |
showLiveMatches |
BOOL | true |
showStandings |
BOOL | true |
showTopScorers |
BOOL | true |
showRecentResults |
BOOL | true |
showUpcoming |
BOOL | true |
refreshInterval |
INT | 300 (seconds) |
createdAt |
TIMESTAMP | auto |
updatedAt |
TIMESTAMP | auto |
ApiRequestLog — append-only log of every outbound API-Football call.
| Column | Type | Description |
|---|---|---|
id |
TEXT (PK) | cuid() |
endpoint |
TEXT | e.g. /fixtures?type=live |
status |
INT | HTTP status code |
duration |
INT | Milliseconds |
createdAt |
TIMESTAMP | Indexed for fast range queries |
Logs older than 7 days are automatically pruned in the background (at most once per hour).
Initialization
On the very first docker compose up, PostgreSQL runs init-db/01-init.sql which creates both tables and inserts a default Settings row. Subsequent starts skip this file. Prisma's db push in start.sh ensures the schema stays in sync.
Connecting directly
docker exec -it primeira_liga_db psql -U primeiraliga -d primeiraliga
Useful SQL:
-- View current settings
SELECT * FROM "Settings";
-- View last 20 API requests
SELECT endpoint, status, duration, "createdAt"
FROM "ApiRequestLog"
ORDER BY "createdAt" DESC
LIMIT 20;
-- Count requests today
SELECT COUNT(*) FROM "ApiRequestLog"
WHERE "createdAt" >= CURRENT_DATE;
Admin Panel
Navigate to http://localhost:3000/admin.
Layout Settings tab
- Site Name — displayed in the browser tab and page header
- Primary Color — used for live indicators, headings, and gradient left side
- Secondary Color — used for gradient right side and accent elements
- Section toggles — individually enable/disable Live Matches, Standings, Top Scorers, Recent Results, Upcoming Fixtures
- Refresh Interval — how often the dashboard auto-refreshes (in seconds; default 300)
API Configuration tab
- API Key — view the currently configured key (masked) or enter a new one. Saved to the database immediately; no restart needed.
- League Switcher — toggle between Primeira Liga 2024/25 (League ID 94) and UEFA Euro 2024 (League ID 4)
Statistics tab
- Total API requests logged
- Requests made today
- Average response time (ms)
- Success rate (%)
- Hourly bar chart of the last 24 hours
- Per-endpoint request breakdown
Performance & Caching
Server-side settings cache
lib/settings.ts caches the Settings row in process memory for 30 seconds. This means API routes don't make a database round-trip on every request. The cache is immediately invalidated whenever settings are updated via PUT /api/admin/settings.
Next.js fetch cache
All outbound calls to API-Football use Next.js's extended fetch with next: { revalidate: N }:
- Live fixtures: revalidate every 30 seconds
- Standings, top scorers, last/next fixtures: revalidate every 5 minutes
HTTP Cache-Control headers
API route responses include Cache-Control: public, s-maxage=N, stale-while-revalidate=2N, making them compatible with CDN edge caching if you front the app with Cloudflare or a similar proxy.
Non-blocking logging
logApiRequest() in lib/api-logger.ts is fire-and-forget — it returns immediately and never adds latency to the response. DB write failures are caught silently.
Parallel database queries
The admin stats endpoint runs all 6 database queries simultaneously with Promise.all, reducing response time from ~6× (sequential) to ~1× (parallel) query latency.
Docker build layer caching
package.json is copied and npm ci is run before any source code is copied, so dependency installation is only re-run when package.json changes (not on every code change).
Docker Reference
Start
docker compose up -d
Stop (keep data)
docker compose down
Stop and delete all data
docker compose down -v
View all logs (live)
docker compose logs -f
View app logs only
docker compose logs -f app
View database logs only
docker compose logs -f db
Rebuild after code changes
docker compose up -d --build
Restart only the app (not the DB)
docker compose restart app
Check container status
docker compose ps
Container names
| Container | Service |
|---|---|
primeira_liga_app |
Next.js application |
primeira_liga_db |
PostgreSQL 17 |
Data Persistence & Backups
Database data lives in the postgres_data Docker volume and persists across docker compose down (without -v).
Backup
docker exec primeira_liga_db \
pg_dump -U primeiraliga primeiraliga > backup_$(date +%Y%m%d).sql
Restore
docker exec -i primeira_liga_db \
psql -U primeiraliga primeiraliga < backup_20260101.sql
Full reset (wipe and start fresh)
docker compose down -v
docker compose up -d
Changing the Port
Edit docker-compose.yml under the app service:
ports:
- "8080:3000" # exposes on host port 8080 instead of 3000
Also update NEXTAUTH_URL to match:
environment:
- NEXTAUTH_URL=http://localhost:8080
Then restart:
docker compose up -d
API Usage & Rate Limits
The app uses API-Football v3.
Free tier limits
- 100 requests per day
- Data updates available every ~15 minutes on the provider side
Default request rate in this app
With the default 5-minute refresh interval and 4 data sections (standings, top scorers, live, last/next fixtures), the app makes roughly 4–5 requests per refresh cycle = ~58–72 requests per 24 hours. This fits within the free tier.
Conserving requests
- Increase the refresh interval in the Admin Panel (e.g. 600 seconds = 10 minutes)
- Disable sections you don't need (e.g. Top Scorers, Upcoming Fixtures)
- The server-side Next.js
fetchcache means multiple browser clients hitting the same route share one upstream request
Monitoring usage
The Admin Panel Statistics tab shows how many requests have been made today and in total.
Troubleshooting
App won't start
docker compose logs -f app
Common causes: Prisma migration failed (DB not ready yet — start.sh waits 5 seconds and the healthcheck retries up to 10 times), or a port conflict.
Database connection error
docker compose logs -f db
Wait 10–15 seconds for the DB to initialise on first run. The app container will retry. If it keeps failing, run docker compose down -v && docker compose up -d to start fresh.
Port 3000 already in use
# Find what's using it
lsof -i :3000
# Or just change the port in docker-compose.yml
API data not loading / blank sections
- Verify your API key is valid at api-football.com
- Check your daily request limit in the API-Football dashboard
- Check app logs:
docker compose logs -f app - Open browser DevTools → Network tab and look for failed requests to
/api/*
Changes in docker-compose.yml not taking effect
docker compose up -d --force-recreate
Code changes not reflected
docker compose up -d --build
Prisma client out of sync after schema change
The start.sh script runs prisma db push --skip-generate on every container start, so schema changes are automatically applied. If you update schema.prisma, rebuild the image.
Development (Local, without Docker)
You can run the Next.js app locally for development while pointing at a local or remote Postgres instance.
1. Install dependencies
cd nextjs_space
# Copy the Docker package.json as the local one
cp package.json.docker package.json
npm install
2. Set up environment
Create nextjs_space/.env.local:
DATABASE_URL="postgresql://primeiraliga:primeiraliga2024@localhost:5432/primeiraliga"
API_FOOTBALL_API_KEY="your_key_here"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="dev-secret-change-in-production"
3. Start a local Postgres (optional — if not using Docker DB)
docker run -d \
--name pg-dev \
-e POSTGRES_USER=primeiraliga \
-e POSTGRES_PASSWORD=primeiraliga2024 \
-e POSTGRES_DB=primeiraliga \
-p 5432:5432 \
postgres:17-alpine
4. Push the schema
cd nextjs_space
npx prisma db push
5. Start the dev server
npm run dev
App available at http://localhost:3000.
Available scripts
| Script | Description |
|---|---|
npm run dev |
Start Next.js dev server with hot reload |
npm run build |
Generates Prisma client and builds for production |
npm run start |
Start the production build |
npm run lint |
Run ESLint |
npm run db:push |
Sync Prisma schema to DB (no migration files) |
npm run db:migrate |
Apply pending Prisma migrations |
System Requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| Docker Engine | 20.10 | Latest stable |
| Docker Compose | v2.0 | Latest stable |
| RAM (for Docker) | 1 GB | 2 GB |
| Disk space | 2 GB | 4 GB |
| CPU | 1 core | 2 cores |
| OS | Linux, macOS, Windows (WSL2) | Linux |