[{"data":1,"prerenderedAt":280},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Fbuilding-invade-lol":22,"\u002Fblog\u002Fbuilding-invade-lol-surround":276},[4],{"title":5,"path":6,"stem":7,"children":8,"page":21},"Blog","\u002Fblog","blog",[9,13,17],{"title":10,"path":11,"stem":12},"Building invade.lol: A League of Legends Stats Platform","\u002Fblog\u002Fbuilding-invade-lol","blog\u002Fbuilding-invade-lol",{"title":14,"path":15,"stem":16},"Nuxt Form Composable","\u002Fblog\u002Fnuxt-form-composable","blog\u002Fnuxt-form-composable",{"title":18,"path":19,"stem":20},"typescript.news: How I Built a Fully Autonomous AI Newsroom","\u002Fblog\u002Ftypescript-news-autonomous-ai-newsroom","blog\u002Ftypescript-news-autonomous-ai-newsroom",false,{"id":23,"title":10,"author":24,"body":26,"date":267,"description":268,"extension":269,"image":270,"meta":271,"minRead":272,"navigation":273,"path":11,"seo":274,"stem":12,"__hash__":275},"blog\u002Fblog\u002Fbuilding-invade-lol.md",{"name":25},"Louis Leschevin",{"type":27,"value":28,"toc":252},"minimark",[29,33,37,47,52,55,68,74,88,92,97,100,108,112,125,128,132,139,154,166,169,173,176,202,206,222,226,235,238,242,249],[30,31,10],"h1",{"id":32},"building-invadelol-a-league-of-legends-stats-platform",[34,35,36],"p",{},"If you play League of Legends, you know the ritual: someone joins your lobby, and you immediately look them up on op.gg or u.gg to see their rank, their main champions, their win rate.",[34,38,39,46],{},[40,41,45],"a",{"href":42,"rel":43},"https:\u002F\u002Finvade.lol",[44],"nofollow","invade.lol"," is my own take on that tool. Search for a summoner, get their profile, match history and analytics, fast and without the clutter. It's open source, and this article covers why I built it and the technical choices behind it.",[48,49,51],"h2",{"id":50},"why-build-yet-another-stats-site","Why build yet another stats site?",[34,53,54],{},"Three reasons, honestly.",[34,56,57,61,62,67],{},[58,59,60],"strong",{},"First, the esport angle."," I've always wanted to work at the intersection of web development and esport. League of Legends has a massive competitive scene and an ",[40,63,66],{"href":64,"rel":65},"https:\u002F\u002Fdeveloper.riotgames.com\u002F",[44],"official API from Riot Games"," that exposes ranked data, match history and detailed match timelines. It's the perfect playground for someone who loves both the game and the data.",[34,69,70,73],{},[58,71,72],{},"Second, I wanted my own version of the tool."," The existing stats sites are powerful but heavy: ads, popups, overlays, and features I never use. I wanted something minimal: a search bar, a profile, the numbers that matter. When you build it yourself, you decide exactly what \"essential\" means.",[34,75,76,79,80,83,84,87],{},[58,77,78],{},"Third, and this is the real reason, I wanted to learn."," Specifically, I wanted a serious project to dig into ",[58,81,82],{},"AdonisJS"," and ",[58,85,86],{},"ClickHouse",", two technologies I kept hearing about but had never pushed into production. A side project you actually use is the best way to learn a stack.",[48,89,91],{"id":90},"the-tech-choices","The tech choices",[93,94,96],"h3",{"id":95},"adonisjs-v6-laravel-energy-typescript-ecosystem","AdonisJS v6: Laravel energy, TypeScript ecosystem",[34,98,99],{},"I've worked a lot with Laravel, and I love its \"batteries included\" philosophy: a real framework with conventions, an ORM, validation, queues, and a CLI, instead of a pile of npm packages glued together.",[34,101,102,107],{},[40,103,106],{"href":104,"rel":105},"https:\u002F\u002Fadonisjs.com\u002F",[44],"AdonisJS v6"," is the closest thing to that experience in the TypeScript world. You get a full MVC framework, fully typed, with first-class support for the things a real application needs. Coming from Laravel, the mental model transfers almost one-to-one, but everything is TypeScript end to end, which means the models I manipulate on the backend are the same types my frontend understands.",[93,109,111],{"id":110},"inertia-vue-a-monolith-that-feels-like-a-spa","Inertia + Vue: a monolith that feels like a SPA",[34,113,114,115,120,121,124],{},"For the frontend, I chose ",[40,116,119],{"href":117,"rel":118},"https:\u002F\u002Finertiajs.com\u002F",[44],"Inertia"," with ",[58,122,123],{},"Vue 3"," rather than building a separate API-consuming SPA.",[34,126,127],{},"Inertia lets the backend drive routing and pass props directly to Vue pages: no duplicated route definitions, no hand-written API client, no state synchronization layer. For a solo project, this cuts the maintenance surface in half: it's one application, deployed as one unit, but the UX is fully client-side and reactive.",[93,129,131],{"id":130},"postgresql-clickhouse-two-databases-two-jobs","PostgreSQL + ClickHouse: two databases, two jobs",[34,133,134,135,138],{},"This is the most interesting architectural decision in the project. invade.lol uses ",[58,136,137],{},"two databases",", each doing what it's best at:",[140,141,142,149],"ul",{},[143,144,145,148],"li",{},[58,146,147],{},"PostgreSQL"," stores the relational data: summoner profiles, accounts, sync state. Classic transactional workload, classic relational database.",[143,150,151,153],{},[58,152,86],{}," stores the match data and powers the analytics.",[34,155,156,157,161,162,165],{},"Why ClickHouse? Because match analytics is an OLAP problem, not an OLTP one. Questions like ",[158,159,160],"em",{},"\"what's this player's win rate per champion over their last 500 games\""," or ",[158,163,164],{},"\"who are their most frequent teammates and how do they perform together\""," are aggregations over large volumes of rows. A columnar database like ClickHouse eats these queries for breakfast, where the same aggregations would make PostgreSQL sweat once the match table grows into the millions.",[34,167,168],{},"Separating the two also keeps each schema clean: normalized relational data on one side, denormalized analytical events on the other.",[93,170,172],{"id":171},"redis-and-cloudflare-r2","Redis and Cloudflare R2",[34,174,175],{},"Two more pieces complete the infrastructure:",[140,177,178,184],{},[143,179,180,183],{},[58,181,182],{},"Redis"," caches the expensive analytics responses. The Riot API has strict rate limits and analytics queries aren't free, so every analytics endpoint is cached. Most profile views never touch ClickHouse at all.",[143,185,186,189,190,195,196,201],{},[58,187,188],{},"S3-compatible object storage"," for assets: ",[40,191,194],{"href":192,"rel":193},"https:\u002F\u002Fmin.io\u002F",[44],"MinIO"," locally, ",[40,197,200],{"href":198,"rel":199},"https:\u002F\u002Fwww.cloudflare.com\u002Fdeveloper-platform\u002Fproducts\u002Fr2\u002F",[44],"Cloudflare R2"," in production. R2's killer feature for a free side project is zero egress fees.",[93,203,205],{"id":204},"developer-experience","Developer experience",[34,207,208,209,212,213,217,218,221],{},"The whole local environment runs with ",[58,210,211],{},"Docker Compose"," (PostgreSQL, ClickHouse, Redis and MinIO), and a Makefile wraps the common operations. ",[214,215,216],"code",{},"make dev"," installs dependencies, boots the services, runs the migrations and starts the dev server. One command from ",[214,219,220],{},"git clone"," to a working stack.",[48,223,225],{"id":224},"open-source","Open source",[34,227,228,229,234],{},"The code is available at ",[40,230,233],{"href":231,"rel":232},"https:\u002F\u002Fgithub.com\u002Finvadelol\u002Fcore",[44],"github.com\u002Finvadelol\u002Fcore",", under the Polyform Noncommercial license: free to read, learn from and use for personal projects, just not to commercialize.",[34,236,237],{},"I open-sourced it because the multi-database setup (PostgreSQL + ClickHouse + Redis behind an AdonisJS API) is the kind of architecture that's easy to read about but rare to see in a complete, runnable project.",[48,239,241],{"id":240},"whats-next","What's next",[34,243,244,245,248],{},"The foundation (search, profile sync, match history, champion and teammate analytics) is live at ",[40,246,45],{"href":42,"rel":247},[44],". The roadmap is mostly about going deeper on the analytics side: more aggregations, better visualizations, and smarter insights from the match timeline data.",[34,250,251],{},"If you try it, look up your own summoner name, and if you read the code and have feedback on the architecture, I'd genuinely love to hear it.",{"title":253,"searchDepth":254,"depth":254,"links":255},"",2,[256,257,265,266],{"id":50,"depth":254,"text":51},{"id":90,"depth":254,"text":91,"children":258},[259,261,262,263,264],{"id":95,"depth":260,"text":96},3,{"id":110,"depth":260,"text":111},{"id":130,"depth":260,"text":131},{"id":171,"depth":260,"text":172},{"id":204,"depth":260,"text":205},{"id":224,"depth":254,"text":225},{"id":240,"depth":254,"text":241},"2026-06-10","Why I built my own op.gg alternative, and the tech choices behind it. AdonisJS v6, Inertia with Vue, PostgreSQL for player data and ClickHouse for match analytics.","md","\u002Fimg\u002Finvade.png",{},8,true,{"title":10,"description":268},"Mlj5yFexPtFXGnx2QCtbVp3g_j3es-sQhmsc3BPjlhQ",[277,278],null,{"title":14,"path":15,"stem":16,"description":279,"children":-1},"Découvrez comment gérer facilement vos formulaires dans Nuxt grâce au module Nuxt Form. Soumissions, transformations et validation simplifiées",1781123525057]