A HIGH game is 4 times as likely as a LOW or unset one, a MEDIUM game twice. Not HIGH first like the picker: the dashboard shuffles by excluding only the current game, so a strict order would swap between two games. The picker now starts a stale Steam sync like the library does.
To Be Played – Core Service
Business service of To Be Played, a Steam backlog tracker. It syncs the user's Steam library, stores the backlog (status, rating, notes, priority), tracks playtime per day, syncs the wishlist and adds time-to-beat data from IGDB.
To Be Played is split into three repositories:
| Repository | Role |
|---|---|
| ToBePlayed-Auth | Users, login, settings, signs JWTs |
| ToBePlayed-Core (this one) | Steam library, backlog, wishlist, stats; verifies JWTs |
| ToBePlayed-Frontend | React single-page app |
Features
- Steam sync: owned games, playtime and achievements through the Steam Web API. Manual sync plus a nightly sync. Resilience4j retries and rate-limits every Steam call; Caffeine caches game counts and player summaries.
- Backlog: status (
BACKLOG,PLAYING,FINISHED,COMPLETED= 100 %,DROPPED), rating 1–5, notes, priority, filters and sorting. "Pile of Shame" summary with a random suggestion of what to play. - Picker: up to 10 random backlog games for a time budget (IGDB normal time to beat), HIGH priority first, then MEDIUM, then the rest. Games already shown can be excluded for a reroll.
- Achievements: after every library sync a background job fetches the achievement schema of each owned game and the user's unlocks of each game whose playtime changed. One Steam call per second, so user requests keep half of the rate limit; it stops after 3 Steam errors in a row or when Steam hides the achievements. A Steam error never overwrites stored data. Summary: unlocked total, perfect games, average completion over started games, the 5 games closest to 100 %.
- Playtime history: one row per game and day, for the activity view and the "most played in the last two weeks" dashboard banner.
- Wishlist: synced from Steam with store price and discount.
- Time to beat: story, extras and completionist hours from the IGDB API. Optional: without credentials the feature is off and the rest works.
- Public profile data: totals, status counts, games in progress and finished games with rating, perfect games and average completion. Readable only with a profile ticket issued by Auth. Never returns notes, priority or the Steam ID.
Authentication
Core never issues tokens. It verifies Auth's EdDSA (Ed25519) JWTs with public keys only. It loads them from Auth's
JWKS by the token's kid and loads again when a new kid shows up (at most every 10 seconds). Loaded keys stay
until Core restarts. JWT_PUBLIC_KEY is used only until the first JWKS load works. The token comes from Auth's
tbp_access cookie or a Bearer header, whichever verifies first. Core reads userId and steamId from the claims.
POST, PUT and DELETE need an X-TBP-Client or Authorization header (CSRF guard), and CORS allows only the
frontend origin.
A token with a typ claim (for example a profile ticket) is rejected on normal endpoints; the profile ticket is
accepted only by GET /api/public/profiles/library in the X-Profile-Ticket header.
API
| Method | Path | Auth |
|---|---|---|
GET |
/api/library/games, /api/library/games/{appid} |
JWT |
PUT |
/api/library/games/{appid}/backlog |
JWT |
PUT |
/api/library/games/{appid}/status (status only, keeps rating, notes, priority) |
JWT |
POST |
/api/library/sync |
JWT |
GET |
/api/library/picker?maxMinutes=&count=&exclude= |
JWT |
GET |
/api/library/achievements/summary |
JWT |
GET |
/api/library/sync-status, /summary, /activity, /wishlist, /steam-profile, /games-count |
JWT |
GET |
/api/public/profiles/library |
profile ticket |
GET |
/api/stats/public |
public |
Tech stack
- Java 25, Spring Boot 4.1 (Web MVC, Security, Data JPA, Validation, Cache with Caffeine), virtual threads
- PostgreSQL 17, Flyway migrations
- Resilience4j: retry and rate limiter for Steam, rate limiter for IGDB
- JJWT with Ed25519 public key
- Spring Boot Actuator + Micrometer (Prometheus)
- Tests: JUnit 5, Testcontainers (PostgreSQL), ArchUnit
- CI: Forgejo Actions, multi-stage Docker image
Architecture
A modular monolith. Each module under core_service.modules is a vertical slice that could move to its own
service later:
| Module | Content |
|---|---|
library |
Sync, backlog, playtime history, wishlist, public profile data |
stats |
Public numbers for the landing page |
steam_adapter |
Steam Web API client |
igdb_adapter |
IGDB client (Twitch OAuth) |
Inside a module: api (controllers, DTOs), service (business logic), domain (JPA entities) and repo
(Spring Data repositories). ArchUnit enforces cycle freedom, controller-to-repository direction, naming and module
isolation in src/test/java/core_service/architecture/ArchitectureTest.java.
Run locally
Needs Java 25 and Docker.
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Spring Boot's Docker Compose support starts PostgreSQL from compose.yaml, Flyway applies the migrations, and the
service listens on http://localhost:8090. Health: /actuator/health, metrics: /actuator/prometheus.
In the dev profile a request without an Authorization header gets a dummy user, so the API is usable without a
running Auth service.
Configuration comes from environment variables:
| Variable | Purpose |
|---|---|
JWT_JWKS_URL |
Auth's /.well-known/jwks.json |
JWT_PUBLIC_KEY |
Ed25519 public key (Base64 of the X.509 DER bytes), used until the first JWKS load. Set this, JWT_JWKS_URL, or both |
STEAM_API_KEY |
Steam Web API key |
IGDB_CLIENT_ID, IGDB_CLIENT_SECRET |
Twitch app for IGDB (optional) |
DB_PASSWORD |
Local PostgreSQL password |
Tests
./mvnw clean verify
Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Needs Docker for Testcontainers.
Roadmap
- Add a game by name from IGDB for platforms without Steam (needs an internal game id next to the Steam
appid). - Mood filter for the picker (needs genres or themes from IGDB).
- Sale alerts for wishlist games.
- Move
steam_adapterDTOs out ofapi, then turn on strict module independence in ArchUnit.
License
All rights reserved. The source is public for reading only.