Building a CS2 Match Platform for Our Friend Group

We have a small CS2 community — just a handful of friends who play together regularly — and the standard matchmaking experience never really fit how we wanted to play. So I built a complete stack around it: a CS2 server plugin, a web platform, and a mobile app, all talking to the same backend. Three projects, one ecosystem.

The Three Pieces

CS2 Server Plugin (C# / .NET 8) — Built on CounterStrikeSharp, this plugin runs inside the dedicated CS2 server. It handles competitive match flow: warmup, .ready system, knife rounds, technical pauses, multi-map series, halftime side swaps, and an "AIM mode" fallback when no match is active. It pulls match configs from a URL given by the web backend and streams every relevant event — kills with weapon and position, round outcomes, chat, lifecycle changes — into MySQL.

Web Platform (Laravel 13 + Vue 3 + Tailwind v4) — The brain of the operation. Players sign in via Steam and Discord (Laravel Socialite), create lobbies, run map veto, and watch matches live. There is zero HTTP polling on the frontend: every state change — lobby status, server boot progress, map ban, live scoreboard — is pushed in real time over WebSockets via Laravel Reverb and Laravel Echo. A scheduler emits score updates once a minute, queue workers handle broadcasts, and Sanctum guards the mobile API. Match history, player stats, and round breakdowns are all rendered from the same data the plugin writes to MySQL.

Mobile App (Kotlin Multiplatform + Compose Multiplatform) — One Kotlin codebase for both Android and iOS. It talks to the Laravel backend over the same Sanctum-protected API and subscribes to the same Reverb channels used by the website, so a map ban happening on someone's laptop appears instantly on a teammate's phone. Push notifications go out through FCM (Android) and APNs (iOS) for events like veto starts, match ready, and lifecycle transitions — handled by laravel-notification-channels/fcm and …/apn on the server side.

How It All Connects

The plugin loads a config from the Laravel backend when a match starts, writes every event back into MySQL, and the same database powers both the web UI and the mobile app — both of which subscribe to identical Reverb channels so all clients stay in sync without anyone refreshing anything.

Tech Stack at a Glance

Layer Technology
Game server plugin C#, .NET 8, CounterStrikeSharp, MySqlConnector
Backend PHP 8.5, Laravel 13, Reverb, Sanctum, Socialite (Steam + Discord)
Frontend Vue 3, Tailwind v4, Vite, Laravel Echo, Chart.js, vue-i18n (EN/UK/PL/BE)
Mobile Kotlin Multiplatform, Compose Multiplatform, FCM, APNs
Realtime Reverb (WebSockets) → Echo on web + KMP client
Testing Pest 4 (PHP), Pint for formatting

Why Build It

Honestly? It's just for fun, and for the people I play with. But it turned into a nice end-to-end exercise — a native game-server extension, a real-time web app, and a cross-platform mobile client all sharing one source of truth. Every match we play also tests every layer of the stack at once, which is the best kind of feedback loop.

Screenshots

1 / 5