r2-gated-downloads-and-play

Order-gated downloads and in-browser play from private R2

The game builds live in a private R2 bucket — nothing in it is publicly readable. The store never exposes the bucket directly; instead it acts as a gate that checks whether a request is entitled to a file before it hands out any way to reach it. There are three distinct channels, and they make deliberately different trade-offs.

Bought downloads: validate, then presign

A buyer's desktop download goes through a dedicated /download/ route rather than a direct link to storage. The route first re-establishes that the request belongs to a real, paid order: it loads the order, compares the caller's order key against the stored one with a constant-time comparison (so a wrong key can't be probed by timing), and requires the order to be in a paid state. Only then does it mint a short-lived SigV4 presigned GET URL for that product's object key and 302-redirect the browser straight to R2.

The point of presigning is that the store never proxies the file bytes — R2 serves the download itself, but only because the store temporarily vouched for this one request, and only for a small window. After the window closes the link is dead. Credentials for signing come from the environment, never from the image or the repo. Build object keys are set per platform (macOS / Windows) by the packaging pipeline, so the same route serves whichever platforms a game actually built.

The free authenticator: same presign, no order

The desktop authenticator app that every game depends on is free, so its /get-authenticator/ route skips the order check entirely and presigns the current build directly. It picks the build channel — development vs production — from the host it is served on, so the two store fronts hand out their own matching builds without any per-request configuration.

Play in browser: never redirect, always re-check

The web-playable build is the interesting case, because a browser game must be reachable by the browser, which is exactly what a leaked presigned URL would let anyone do. So /play/ refuses to hand the client any link to storage at all. On every load it re-checks that the signed-in account owns this game — the same ownership source that drives the "My Games" list — then fetches the private web build server-side and streams the self-contained game HTML back through itself. The presigned URL is created and consumed entirely on the server and never reaches the client. This is the deliberate asymmetry: a download link is fine to expire in the wild, but a play URL that could be shared would bypass the gate, so it is simply never emitted.

Why it's shaped this way

Two of the three channels lean on presign-and-redirect because it is cheap: the store stays out of the byte path. The third can't, because the thing being delivered is the entry point to the content, not an archive of it. The gate therefore moves from "prove you paid, here's a fading key" to "prove you own it, and I'll read it to you myself." Ownership is keyed on the account's email, which is what ties an order to an identity across both the store and the entitlements the games app reads.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

r2-gated-downloads-and-play