Jamie PackerProjects

Copyright © 2026 Jamie Packer. All rights reserved.

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.

Google Cloud PlatformData EngineeringEvent-Driven ArchitecturePythonCI/CDCounter-Strike
Live Site
Counter-Strike Analytics Platform screenshot

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.

Sped-up walkthrough of uploading a demo and exploring the match page
Upload → processing → match page (sped up).

How it works today

The live pipeline:

  1. A user uploads a match recording from the browser
  2. The file lands in cloud storage via a presigned URL
  3. Object finalize triggers Pub/Sub
  4. A Cloud Run worker parses the demo and extracts stats
  5. 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.

High-level architecture diagram of the CS-Bait MVP pipeline
MVP architecture: browser upload → GCS → Pub/Sub → Cloud Run → Silver results.

Architecture

An event-driven, serverless data pipeline on Google Cloud Platform, loosely following a medallion / lakehouse pattern:

LayerWhat lives thereRole
BronzeRaw uploaded demo filesTemporary landing zone
SilverPer-match JSON resultsFrontend-ready analytics payload
OLTP (planned)Users, jobs, match index, fact rowsApp database for Steam-era product state (not a medallion tier)
Gold (planned)Aggregated cross-match insightsLeaderboards, 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: demoparser2 for 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 uploadId for job tracking, and a content fingerprint (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.