A self-hosted dashboard for tracking your Steam library, monitoring achievement progress, and hunting down completions.
- Features
- Tech Stack
- Prerequisites
- Getting Started
- Environment Variables
- Scripts
- Architecture
- API Reference
- Authentication
- Database
- Deployment
- Contributing
- License
- Achievement tracking — pending, unlocked, and completion status per game
- Library analytics — playtime, perfect games, average completion, and indexed totals
- Steam profile badges — level badge (official sprites), years of service, and Game Collector with tooltips
- Recently played — sorted by actual last played time
- Filterable library — multi-toggle state filters (In Progress, Perfect, Untouched), sort options, achievements toggle
- Game detail pages — pending/unlocked tabs, progress bar, unlock timestamps, Steam Store link
- Image discovery — automatically probes Steam CDN for the best available game art
- Multi-user ready — whitelist-based access via Steam OpenID, data fully isolated per user
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict mode) |
| UI | React 19 + Tailwind CSS 4 + shadcn/ui |
| Database | SQLite via Node.js built-in DatabaseSync |
| Auth | Steam OpenID 2.0 |
| Testing | Vitest + Testing Library |
| Linting | ESLint + Prettier |
| CI/CD | GitHub Actions |
| Package Manager | pnpm 10.25 |
- Node.js
24.13.1(see.nvmrc) - pnpm
10.25.0
# Clone and install
git clone https://github.com/finallyjay/steam-backlog-hunter.git
cd steam-backlog-hunter
nvm use
pnpm install
# Configure environment
cp .env.local.example .env.local
# Edit .env.local with your values (see Environment Variables below)
# Start development server
pnpm devThe app will be available at http://localhost:3000.
| Variable | Required | Description |
|---|---|---|
STEAM_API_KEY |
Yes | Steam Web API key (get one here) |
ADMIN_STEAM_ID |
Yes | Steam64 ID with admin access (always allowed to sign in + /admin) |
NEXTAUTH_URL |
Production | Your app's public URL (e.g. https://steam.example.com) |
SQLITE_PATH |
No | Custom SQLite database path (see Database) |
STEAM_WHITELIST_IDS |
No | Comma-separated Steam64 IDs for initial seed (managed via /admin after) |
LOG_LEVEL |
No | Pino log level (default: info) |
| Command | Description |
|---|---|
pnpm dev |
Start development server |
pnpm build |
Production build (standalone output) |
pnpm start |
Run production server |
pnpm lint |
ESLint + TypeScript typecheck |
pnpm test |
Run test suite (Vitest) |
pnpm typecheck |
Type generation + tsc --noEmit |
pnpm format |
Format codebase with Prettier |
pnpm format:check |
Check formatting without writing |
Steam API → SQLite → API Routes → Client Hooks → UI
app/
├── api/ # API routes
│ ├── auth/ # Steam OpenID login flow
│ ├── steam/ # Data endpoints (games, achievements, stats, sync)
│ └── health/ # Infrastructure health check
├── dashboard/ # Dashboard page + error boundary
├── games/ # Library page with filters
├── game/[id]/ # Game detail page + error boundary
└── page.tsx # Landing / login
components/
├── dashboard/ # Dashboard-specific components
└── ui/ # Reusable UI primitives (shadcn/ui)
hooks/ # Custom React hooks (user state, data fetching)
lib/
├── server/ # Server-only modules (marked with "server-only")
│ ├── sqlite.ts # Database schema and versioned migrations
│ ├── steam-games-sync.ts # Game ownership sync
│ ├── steam-achievements-sync.ts # Achievement data sync
│ ├── steam-stats-compute.ts # Stats aggregation
│ ├── steam-images.ts # Image discovery and probing
│ ├── rate-limit.ts # In-memory rate limiter
│ └── logger.ts # Structured logging (Pino)
├── steam-api.ts # Direct Steam Web API calls
├── env.ts # Zod-validated environment variables
├── whitelist.ts # Steam ID whitelist enforcement
└── types/ # TypeScript interfaces
- Steam API is queried when data is stale or a manual refresh is triggered
- SQLite stores all persistent data (games, achievements, schemas, stats, images)
- API routes serve data from SQLite, triggering syncs when needed
- Client hooks manage fetching, caching, deduplication, and cooldowns
- UI components consume hooks and render the dashboard
| Data | TTL |
|---|---|
| Owned games | 24 hours |
| Achievements | 7 days |
| Game schemas | 30 days |
| Stats snapshot | 15 minutes |
| Game images | 30 days |
All data endpoints require authentication via steam_user httpOnly cookie. JSDoc documentation is available on every route handler and public function.
| Method | Path | Description |
|---|---|---|
GET |
/api/auth/steam |
Initiate Steam OpenID login (rate limited: 10/min per IP) |
GET |
/api/auth/steam/callback |
Handle OpenID callback (CSRF nonce validated) |
POST |
/api/auth/logout |
Clear session cookie |
GET |
/api/auth/me |
Get current authenticated user |
| Method | Path | Description |
|---|---|---|
GET |
/api/steam/games?type=recent|all |
List owned or recently played games |
GET |
/api/steam/achievements?appId=N |
Get achievements for a game |
GET |
/api/steam/achievements/batch?appIds=1,2,3 |
Batch get achievements (max 200) |
GET |
/api/steam/game/:id |
Get single game details |
GET |
/api/steam/stats |
Get aggregated user stats |
GET |
/api/steam/sync |
Get last sync timestamps |
POST |
/api/steam/sync |
Trigger full data sync (rate limited: 5/min per user) |
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check (SQLite connectivity, no auth) |
Steam OpenID 2.0 with CSRF nonce protection and timing-safe validation.
STEAM_WHITELIST_IDScontrols who can sign in (comma-separated Steam64 IDs)- If missing or empty, all access is denied
- Sessions stored in httpOnly, secure, SameSite cookies (7-day expiry)
- Whitelist is re-validated on every server auth check
- Each user's data is fully isolated by
steam_id - Steam level and community badges are fetched at login
SQLite is the primary store. The schema auto-initializes on first run.
Schema changes are managed through a versioned migration system (lib/server/sqlite.ts). A schema_migrations table tracks which migrations have run. Each migration executes exactly once, in order, inside a transaction.
To add a new migration, append a function to the migrations array in sqlite.ts. Never modify existing migrations.
SQLITE_PATHenvironment variable/data/steam-backlog-hunter.sqlite(if/datais writable — containerized deployments).data/steam-backlog-hunter.sqlite(project directory fallback)
steam_profile · games · user_games · recent_games_snapshot · stats_snapshot · schema_migrations
All user-specific tables are keyed by steam_id for multi-user isolation.
- Set environment variables in your deployment platform
- Mount a persistent volume for SQLite (set
SQLITE_PATHor mount at/data/) - The app builds as a standalone Next.js output
pnpm build
pnpm startEnsure NEXTAUTH_URL matches your public URL for Steam OpenID redirects.
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Commit using Conventional Commits (
feat:,fix:,chore:, etc.) - Pre-commit hooks will run Prettier and ESLint automatically
- Create an issue first, then open a Pull Request that closes it
CI runs lint → test → build on all PRs.
This project is licensed under the MIT License.