CSReplays
·5 min read

How auto-sync works: from "leave the server" to "report is ready"

A walkthrough of the pipeline that turns a finished CS2 match into a finished coaching report without the user touching anything — including the parts where it currently does not yet.

The promise

For Pro users, auto-sync is the entire pitch: play CS2 like you normally do, and reports show up in the dashboard within 10 minutes of leaving the server. No Watch tab, no download icon, no upload form.

The promise is simple. The pipeline behind it is the part that is interesting.

The path a match takes

A finished match touches six systems before it becomes a report. Roughly in order:

1. Steam — sharecode tracker. Once you connect Steam, a small worker polls the standard match-history API and watches for new sharecodes (Valve's opaque per-match identifier). Sharecodes are how CS2 surfaces matches that have a demo file attached. 2. Valve CDN — demo download. When a new sharecode appears, the worker resolves it to a download URL and pulls the .dem. Demos are typically 100–300 MB. They are streamed straight into our object storage so the worker never has to hold the whole file in memory. 3. Object storage — Cloudflare R2. Demos live in encrypted, account-scoped buckets. The webapp only ever sees signed URLs. Multipart upload is used for any file above the part-size threshold so a slow connection doesn't lose progress. 4. Inngest — durable queue. The webapp emits an analyze.requested event when a new demo lands. Inngest is the queue and the scheduler. It guarantees the event gets processed once and gives us retries, dead-lettering, and a UI for inspecting failed runs without rolling our own. 5. Modal — Python worker. The actual demo parsing runs on Modal. It is serverless, pay-per-second, and scales to zero — exactly the right shape for our load: bursty, ~150 seconds per job, mostly idle. There is a ~3-second cold start on the first request after idle, which we eat in exchange for paying nothing while no one is uploading. 6. Postgres + Resend. The worker writes the finished digest and report into Supabase Postgres, marks the analysis complete, and Resend fires a transactional email to the user. The UI re-renders the dashboard with the new entry.

What the worker actually does

The Modal worker is a single function. It takes one job, runs the pipeline end-to-end, and exits. Inside, it does:

  • Parse the .dem with awpy, our Python demo-parsing library of choice. The output is a few hundred thousand tick-level events.
  • Build the digest. This is the part that is the actual product. The digest is a structured summary of the match — per-player stats, per-round event summaries, smoke/molly matches against our lineup catalog, cross-round patterns ("over-rotated to B on T-side three rounds in a row after losing first contact"). Target size: 1–2k tokens.
  • Build round details. A separate, larger artifact: every player's position at every tick of every round, used to drive the scrubbable radar replay on the frontend.
  • Call Claude. With the digest and a coaching system prompt we have iterated on locally. The output is the coaching report — about 500 words, round-anchored, in plain English.
  • Verify. A post-processing step parses the report's round references and verifies each one actually exists in the digest. If the model invented a round, we reject and retry. This is the only reason hallucination has not been a product-killing problem.
  • Persist. Digest, round details, report, and a few summary stats land in Postgres. The blob storage is left alone — we can re-parse any demo from scratch if our digest format changes.

What is finished, and what is not

Live today:

  • Manual upload through the entire pipeline. Demo in, report out.
  • Inngest queue, Modal worker, R2 storage, Postgres, Resend email.
  • Hallucination guard.

Not yet live, but in this branch:

  • The Steam sharecode tracker. It works against the official API but needs a real Pro user to test the end-to-end loop in production.
  • A second service ("steambot") for users who want their FACEIT matches auto-synced too. FACEIT has its own demo CDN and its own OAuth dance; that adds a service rather than a path through an existing one.

Why this shape

A reasonable question: why not just put the worker on the same box as the webapp? Two reasons.

First, demo parsing is CPU- and memory-heavy and bursty. Co-locating it with a Next.js process means we either over-provision the webapp constantly or kill it the first time three users upload at once. Modal at $0 idle and 150s of compute per job is the right cost shape.

Second, isolating the worker means the webapp can deploy ten times an hour without affecting any in-flight analysis. Inngest holds the queue. The worker keeps draining. Vercel can do its thing.

That is the whole pipeline. There is a lot of code, but it is mostly glue, and the glue gets out of the way once the digest is the thing being iterated on.

How auto-sync works: from "leave the server" to "report is ready" · CSReplays