A League of Legends statistics API built with AdonisJS v6.
Personal project. I wanted a fast, no-fluff League of Legends stats app, so I built one. The live version runs at invade.lol - feel free to try it out before setting up locally.
Not affiliated with or endorsed by Riot Games. League of Legends is a trademark of Riot Games, Inc.
- Framework - AdonisJS v6
- Databases - PostgreSQL (player data) + ClickHouse (match analytics)
- Cache - Redis
- Storage - S3-compatible (MinIO locally, Cloudflare R2 in production)
- Data source - Riot Games API
Search still accepts Name#Tag, and profiles keep the /:player URL
(/<GameName>-<TagLine>). No region selector is needed. Typeahead searches
stored players across regions, including non-Latin names; entering a full Riot ID
also resolves players who are not stored yet.
The backend resolves the Riot ID to a PUUID, then checks supported League platforms until it finds the profile. A saved platform or a recognizable tag is only a priority hint, not an authoritative region. Custom tags work too. Only 404 responses advance to another platform; key, rate-limit, and service errors are preserved. Concurrent lookups for the same ID are coalesced, and successful results are persisted so subsequent visits avoid discovery calls.
Account routing is separate from match routing (Oceania and Southeast Asia use SEA for matches). PH2 and TH2 are normalized to SG2. Refresh detects server transfers and routes match requests using the newly resolved platform. See Riot's routing documentation.
GET /api/summoners/:summoner detects the platform automatically. The existing
GET /api/summoners/:platform/:summoner API remains available. For
POST /api/summoners/sync, omit platform to detect it automatically; a valid
profile with no new matches returns 200 with an empty matches array.
Run node ace migration:run when upgrading to add the index used by regionless
profile lookups. Keep RIOT_API_KEY in the server environment or ignored .env.
- Node.js >= 22
- Docker and Docker Compose
- A Riot Games API key - get one at https://developer.riotgames.com
1. Clone
git clone https://github.com/invadelol/core.git
cd core2. Fill in secrets
make devOn first run, make dev creates .env from .env.example and stops.
Open .env and fill in two values:
APP_KEY- runnode ace generate:keyto get oneRIOT_API_KEY- get a key at https://developer.riotgames.com
Everything else is pre-filled with local defaults.
3. Start
make devThat's it. make dev installs dependencies, starts Docker services (PostgreSQL,
ClickHouse, Redis, MinIO), waits for them to be ready, runs migrations, and
launches the dev server.
The API is available at http://localhost:3333.
| Command | Description |
|---|---|
make dev |
Full onboarding + dev server (use this) |
make up |
Start Docker services only |
make down |
Stop Docker services |
make migrate |
Run all migrations |
make build |
Build for production |
make start |
Start production server |
make test |
Run test suite |
make lint |
Run ESLint |
make typecheck |
TypeScript check |
npm run dev # dev server with HMR
npm run build # production build
npm start # production server
npm run lint # ESLint
npm run format # Prettier
npm run typecheck # TypeScript check
npm test # test suite| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
The website's home page |
GET |
/api/health |
Health check |
GET |
/docs |
Swagger UI |
GET |
/swagger |
OpenAPI spec |
The JSON routes below are served under /api (/api/summoners/search, /api/matches/:id…).
| Method | Endpoint | Description |
|---|---|---|
GET |
/summoners/search?q= |
Search by name |
POST |
/summoners/sync |
Sync from Riot API |
GET |
/summoners/:platform/:summoner |
Get by GameName-TagLine |
All routes require a valid PUUID and are cached.
| Method | Endpoint | Description |
|---|---|---|
GET |
/summoners/puuid/:puuid/activity |
Match activity |
GET |
/summoners/puuid/:puuid/friends |
Frequent teammates |
GET |
/summoners/puuid/:puuid/ranks |
Current and historical ranks |
GET |
/summoners/puuid/:puuid/stats |
Overall stats |
GET |
/summoners/puuid/:puuid/champions |
Per-champion stats |
GET |
/summoners/puuid/:puuid/matches |
Recent matches (count, offset, filters); before=<epoch ms> pages older games, stored pages are free; a short unfiltered page is filled from Riot at most 3 times right away, then 1 per 2 min per player and IP (12, then 1 per 30 s per IP). Over that (or under Riot's own limit), 200 with the stored games only, possibly none, and Retry-After: the list ends quietly for now |
GET |
/matches/:id |
One match in full, with its timeline |
PUT |
/summoners/puuid/:puuid/increment |
Increment profile view count |
The Invade desktop app onboards players, uploads the games they finish, and
reports the players the League client shows it (ranks, mastery, icon), so the
website stays fresh with fewer Riot calls. The full contract, shared with the
app, is docs/desktop-sync.md. Contract v1.3 (games the app only
observed, POST /api/desktop/matches/known, compressed bodies), which desktop app 0.2.9 uses,
is in invadelol/core#20.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/desktop/config |
- | Kill switches and the 2 MB upload limit |
POST |
/api/desktop/devices |
- | Register a device; returns its inv_dev_ token and signing secret once (10/h per IP) |
DELETE |
/api/desktop/devices/me |
signed | Revoke this device |
POST |
/api/desktop/link |
signed | Make an account one of the device's uploaders (20/h) |
DELETE |
/api/desktop/link/:puuid |
signed | Unlink an account |
GET |
/api/desktop/resolve?gameName=&tagLine= |
optional | Onboard by Riot ID from stored data (30/h per device, 20/h per IP) |
POST |
/api/desktop/matches |
signed | Upload a finished game (30/h per device, 120/h per IP, 2 MB) |
POST |
/api/desktop/players |
signed | Report up to 25 players the client showed (60 requests and 1500 players/h per device, 240/h per IP, 256 KB) |
"Signed" means a bearer token plus X-Invade-Timestamp and an HMAC-SHA256
X-Invade-Signature over method, path, query and body (contract §1.1); stale
timestamps and replays are refused. Only the token's SHA-256 is stored, and the
secret only encrypted with APP_KEY (rotating the key makes every device
register again).
A game already stored costs no Riot call, nor does one from a trusted device
(five games Riot confirmed, on three different days, from a device at least
three days old); anything else costs one getMatchById, and a game Riot has
not published yet is retried in-process 2, 5 and 15 minutes later. Player
reports are mapped to profiles without Riot calls, applied only when the device
is trusted, the player is its own account, or a second device elsewhere agrees,
and never over newer data. Ranks reported in the last 10 minutes (3 for the
Update button) and mastery reported in the last day spare league-v4 and
champion-mastery-v4. One trusted game in ten and one applied player report in
twenty are checked against Riot afterwards; one lie revokes the device and
deletes what it wrote. Desktop data shows its provenance (source,
observedAt) in the API and on the website. The code lives in
app/services/desktop/.
Run node ace migration:run to create the tables (desktop_device,
desktop_device_account, riot_puuid_alias, desktop_match_upload,
match_source, lp_change, player_observation, player_mastery,
riot_rank_confirmed, and riot_rank.source / riot_rank.device_id). No new
environment variables: DESKTOP_UPLOADS_ENABLED also pauses player reports.
Tests: node ace test --tags=@desktop runs the rules (conversion, structural
checks, LP, identity mapping, publication policy, player snapshots, signing,
trust and rollback, auth, rate limits) and the routes end to end
(tests/functional/desktop_*.spec.ts). They use the databases and Redis in
.env with Riot stubbed, so point DB_DATABASE, CLICKHOUSE_DB and
REDIS_DB at throwaway ones.
Every image the site and the desktop app draw comes from /cdn, a caching
proxy in front of Data Dragon and Community Dragon: allow-listed kinds and id
shapes only, one upstream fetch per asset per patch, mirrored to the bucket.
Kinds, upstream mapping, caching and limits: docs/cdn.md.
| Method | Endpoint | Description |
|---|---|---|
GET |
/cdn/:patch/:kind/:id |
An asset pinned to a patch (/cdn/16.20/champion/266.png), immutable |
GET |
/cdn/:kind/:id |
The same on the live patch, cached for a day |
GET |
/cdn/names/:kind |
Id → display name for champions, items, spells, runes, augments |
The site has a light and a dark theme, both from the package's tokens. The
theme follows the operating system by default and can be set explicitly from
the header; the choice is stored in localStorage and replayed before first
paint, so there is no flash.
Matches and profiles are drawn by @invadelol/ui, the
same components as the desktop app. The package is on npm; the site installs a built copy from
vendor/invadelol-ui (a file: dependency), so a package change can ship here before it is
released. After a change in the package, refresh it and commit vendor/:
node scripts/sync_ui.mjs ../ui # path to the ui checkout (default ../ui, or $INVADE_UI)See docs/design.md for the tokens, themes and primitives, and docs/player-profile.md for what each page shows.
| Variable | Required | Description |
|---|---|---|
APP_KEY |
yes | AdonisJS signing key - run node ace generate:key |
RIOT_API_KEY |
yes | Riot developer API key |
DB_HOST |
yes | PostgreSQL host |
DB_PORT |
yes | PostgreSQL port |
DB_USER |
yes | PostgreSQL user |
DB_PASSWORD |
no | PostgreSQL password |
DB_DATABASE |
yes | PostgreSQL database name |
CLICKHOUSE_URL |
yes | ClickHouse HTTP endpoint |
CLICKHOUSE_USER |
yes | ClickHouse user |
CLICKHOUSE_PASSWORD |
no | ClickHouse password |
CLICKHOUSE_DB |
yes | ClickHouse database name |
DRIVE_DISK |
yes | Storage driver - only r2 is supported |
R2_KEY |
yes | S3/R2 access key |
R2_SECRET |
yes | S3/R2 secret |
R2_BUCKET |
yes | S3/R2 bucket name |
R2_ENDPOINT |
yes | S3/R2 endpoint URL |
REDIS_HOST |
yes | Redis host |
REDIS_PORT |
yes | Redis port |
REDIS_PASSWORD |
no | Redis password |
REDIS_DB |
no | Redis logical database (default 0) |
DESKTOP_UPLOADS_ENABLED |
no | Desktop game uploads on/off (default true) |
DESKTOP_RESOLVE_ENABLED |
no | Desktop Riot ID onboarding on/off (default true) |
DESKTOP_MIN_APP_VERSION |
no | Oldest desktop app version told to upload (default none) |
See .env.example for all defaults.
Polyform Noncommercial 1.0.0 - free for personal and open source use, not for commercial use.