Counter-Strike Analytics Platform
July 2026
An event-driven, serverless analytics platform on GCP that parses Counter-Strike demos into deep per-match stats. Pipeline, frontend, and CI/CD are live.

What is this?
Most Counter-Strike analytics tools take a match recording (known as a “demo” file, a replay of everything that happened in a game) and produce personal performance stats: kills, deaths, accuracy, and so on.
This project uses the same data and presents many of the same stats, but with some much more bespoke ones too, aimed at further quantifying a player’s style of play. The long-term goal is a light-hearted analytics product that pokes fun at less-altruistic players.
It is partly a cloud data-engineering learning project, and partly something I want friends (and potentially other players) to actually use. It also sits downstream of my earlier behavioural modelling work: that project asked whether playstyle is measurable; this one is about shipping the pipeline and product surface that can make use of those ideas.
Working title: CS-Bait (referencing using one’s teammates as “bait”). Final name and domain are TBD; the live demo currently sits on a temporary Vercel URL (linked above). The source is private, so architecture and design choices are documented here instead of a public repo.
Status: Work in progress. The end-to-end pipeline is live, with automated tests, automatic deploys, and match pages that surface stats you do not get from the in-game scoreboard (nor from existing similar platforms). Steam integration, a better Legendaryness model, and richer playstyle metrics are still ahead.

How it works today
The live pipeline:
- A user uploads a match recording from the browser
- The file lands in cloud storage via a presigned URL
- Object finalize triggers Pub/Sub
- A Cloud Run worker parses the demo and extracts stats
- The frontend polls for status, then shows a match page when results are ready
Upload → storage event → queue → processor → results.
Beyond the usual K/D/ADR, the match page surfaces side-split views of opening attempts, first-contact timing, losses survived (“saves”), and last-alive-on-wipe counts, plus a per-round timeline, a round-by-round opening-duel log, and a Legendaryness score (v0.1, computed in the browser today) that ranks how aggressively someone threw themselves into the fight. That kind of depth is not available from the game UI after a match; it has to be reconstructed from the demo.

Architecture
An event-driven, serverless data pipeline on Google Cloud Platform, loosely following a medallion / lakehouse pattern:
| Layer | What lives there | Role |
|---|---|---|
| Bronze | Raw uploaded demo files | Temporary landing zone |
| Silver | Per-match JSON results | Frontend-ready analytics payload |
| OLTP (planned) | Users, jobs, match index, fact rows | App database for Steam-era product state (not a medallion tier) |
| Gold (planned) | Aggregated cross-match insights | Leaderboards, trends, career rollups |
Silver is the per-match document store today. An OLTP database comes first once Steam auth lands (users, ingestion jobs, and SQL-friendly facts). Gold can then be derived from those facts; a warehouse copy is optional later and is not on the parse hot path.
Current stack
- Frontend: Next.js (App Router) on Vercel
- Upload path: Presigned URL to GCS (the app server does not shuttle large files)
- Storage: GCS buckets for raw demos, job status, and processed results
- Messaging: Pub/Sub on object finalize, with push delivery and a dead-letter queue
- Compute: Cloud Run (Python / FastAPI), scale-to-zero when idle
- Parsing / analytics:
demoparser2for demo parsing, Polars for feature extraction - Module boundaries: parse → intermediate model → feature extraction → orchestration / I/O
- CI/CD: GitHub Actions: unit tests on PR/push; on
main, build/push the image to Artifact Registry and deploy Cloud Run
Design choices
- Two IDs: an
uploadIdfor job tracking, and a contentfingerprint(checksum + size) for dedupe and the final match URL - Schema versioning on Silver results, so outputs can be reprocessed when the analytics model changes
- Fingerprint-level duplicate handling: the same file is not fully reprocessed if a current result already exists
- Raw demos are deleted after processing (with a Bronze lifecycle safety net) to keep storage costs down
- Results can be fetched directly from GCS, so the match page does not need to go through the app server
- Cost-aware defaults: scale-to-zero compute and a lean container, so idle time does not burn money
What’s next
1. Steam auth + OLTP
Manual upload worked for the MVP, but demos are large and high-friction to upload every match. Steam auth plus backend demo download would make the product much easier to use, and it needs an app database for users, jobs, and a match index. I am leaning toward Neon (serverless Postgres) over always-on Cloud SQL or a document store that fights SQL aggregates. Match pages would keep serving Silver JSON from GCS; the OLTP would hold pointers and compact fact rows. Once demos arrive server-side, status can live in account-backed history instead of the temporary upload-status polling the browser owns today. A Gold / warehouse layer stays optional and later.
2. Legendaryness and playstyle depth
Legendaryness v0.1 already ships on the match page (Legends/Losers tab), but it is still a rough client-side score. Next steps are to harden that model, move the computation into the backend so Silver carries the result, and add more bespoke playstyle / role-centric metrics that further set the product apart from existing platforms.