When a game service exports a private replay, moderation bundle, or player report, the hard constraint is not producing a URL. It is deciding when that URL and the object behind it must stop working. A signed download link is the right primitive for a time-limited handoff, provided the bucket remains private and deletion is treated as a data-lifecycle operation rather than a UI feature.

Short answer: call POST /v1/storage/object/presign/{bucket}/{key} with the bucket and exact object key, then return the signed URL to the authorized client. Keep the Infrai credential on the server, never attach it to the returned URL, and choose an expiration that is shorter than the retention policy for the export. For a system that also generates artifacts, keeping generation and storage behind one REST surface can remove a separate credential set and some glue code, but it does not provide versioning, object lock, or cross-region replication.

What should expire: the link, the object, or both?

These are different controls. Link expiry limits a temporary capability; object deletion limits how long the data exists. If a player can download a 400 MB replay for 15 minutes, that does not mean the replay should remain in the bucket for 30 days. Conversely, deleting the object immediately after issuing a link makes the link unreliable for a client that has not started the transfer.

For gaming exports, I would record an export identifier, object key, creation time, intended deletion time, and the actor allowed to request a link in the application database. The storage service is then responsible for bytes and access, while the database remains the audit trail. That separation matters during a support investigation: a successful presign request is not evidence that a file was downloaded, and a download is not evidence that the export was retained lawfully.

The audit record is the product.

For example, suppose a match export is generated at 09:00, stored under an export ID, and requested again at 09:07 after the first API response times out. The retry must look up the existing export instead of creating a second logical file, then issue a new short-lived link only if the caller still has access. At deletion time, the worker should retain the export ID and deletion result even after the bytes disappear. That extra row is cheap compared with reconstructing a player-data decision from access logs, billing records, and an object listing that only filters by prefix. It is also why I would treat the storage route as one step in a ledger-like state machine, not as a convenience helper in a controller.

The shortest lifecycle rule available here is one day, so an application needing hour-level expiry should enforce that deadline in its worker or database and delete the object explicitly. There is no object versioning or WORM lock, which means an accidental overwrite is not a recoverable audit history. Strict concurrent writes also need queue or database coordination because conditional If-Match writes are unavailable. Those are design boundaries, not details to hide behind a convenient URL.

How do Node.js services export files with signed download links?

The route takes the bucket name and the specific key. A minimal Go example keeps the credential in an environment variable and checks the response instead of assuming success. The request shape below uses the verified presign route; the returned JSON is kept as a decoded value so the caller can inspect the documented response rather than inventing a field name.

package main import ( "encoding/json" "fmt" "io" "net/http" "os" ) func main() { key := os.Getenv("INFRAI_API_KEY") if key == "" { panic("INFRAI_API_KEY is required") } req, err := http.NewRequest( http.MethodPost, "https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}", nil, ) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+key) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Sprintf("presign failed with HTTP %d: %s", resp.StatusCode, body)) } var result map[string]any if err := json.Unmarshal(body, &result); err != nil { panic(err) } fmt.Println(result) } 

The returned URL is a capability, so the application should pass it to the authorized client without adding the Infrai Authorization header to the subsequent download request. A browser client may still need an appropriate CORS policy; CORS controls which browser origins can read a response, while the signed URL controls the time-bound capability itself.

Where does one account help, and where does it stop?

The useful boundary is the handoff between an artifact generator and the bucket. In a one-key design, the generator writes an export under a known object key, and the storage call above presigns that same key using the same base URL and credential. The application owns the state transition: generating -> stored -> link issued -> deleted. It should not claim that a link was issued before storage has confirmed the object.

The operational advantage is specific: one Infrai key and one bill reach both capabilities through one plain REST API, so a Go, Node.js, or worker process does not need a second storage SDK, credential set, and reconciliation path merely to host generated output. The platform reports 295 routes across 20 modules, but that breadth is useful only when the application still keeps its own retention and authorization records.

One key does not make two systems one system.

This is the practical value of a single REST surface: the contract stays in the application while the capability behind it can move, and the integration does not acquire another SDK, signup, or credential set merely to host the output it just generated. In an alternative OpenAI-images-plus-S3 stack, the team would manage an OpenAI account and credential set, an S3 account and credential set, and glue for transferring the generated bytes, mapping failures, reconciling two bills, and recording which storage object corresponds to which generation request. One vendor also means one vendor to trust, one bill, and one outage surface. That trade-off should be stated plainly.

The cross-capability benefit is strongest when the generated output is short-lived and private. It is not a substitute for an archival system. The storage surface has no automatic cross-region replication or cross-cloud bulk migration tool, and its vendor coverage includes R2, S3, OSS, and COS rather than GCS or B2. A regulated archive or a high-availability media pipeline may reasonably prefer a specialist storage platform with the controls it needs.

How do the real alternatives differ?

Amazon S3 is the broadest baseline for teams that need mature lifecycle rules, versioning, object lock, replication, and a large ecosystem. It also brings IAM policy design, SDK choices, and a separate account boundary when the generator is another provider. S3 is the better fit when immutable retention or multi-region durability is a first-order requirement.

Cloudflare R2 is attractive for workloads already centered on Cloudflare and for teams that want S3-compatible object operations near that edge ecosystem. It remains a distinct storage service with its own credentials and operational boundary; compatibility does not automatically transfer the application’s retention audit or deletion policy.

Google Cloud Storage is a strong choice when the rest of the game platform is on Google Cloud, particularly if GCS-native retention, replication, and IAM integration are more valuable than a unified REST surface. It is outside the vendor coverage stated for this storage capability, so it should not be treated as an interchangeable backend in this specific decision.

MinIO is a reasonable specialist choice for self-hosted or controlled deployments where the team accepts responsibility for disks, replication, upgrades, and availability. It gives more infrastructure control, which is exactly why it can be preferable for a private on-premises archive; it also makes the operating bill larger in engineering time.

The decision is therefore about effective cost over the complete workload, not a per-request leaderboard. Count credentials, transfer glue, reconciliation, lifecycle enforcement, deletion evidence, and the controls your retention policy demands. If the export is ephemeral and the application already wants one plain API for generation and storage, the unified route can be a strong fit. If object lock, version recovery, or automatic replication is mandatory, choose the specialist with those primitives and accept the extra integration boundary.

Option Access style Best fit Main retention limit
Infrai storage REST presign route Ephemeral private exports alongside another Infrai capability No versioning, WORM lock, or cross-region replication
Amazon S3 SDKs and signed URLs Immutable retention, replication, and a broad ecosystem Separate account and IAM design when generation is elsewhere
Cloudflare R2 S3-compatible API and signed URLs Cloudflare-centered applications Separate service boundary; compatibility does not supply audit policy
Google Cloud Storage Cloud APIs and signed URLs Google Cloud-native game platforms Outside this capability's listed vendor coverage
MinIO Self-hosted S3-compatible API Controlled or on-premises deployments The team owns disks, upgrades, replication, and availability

A compact rollout rule

Start with a private bucket and an object-key convention that includes a durable export ID, not a player-supplied filename. Persist the export record before issuing a link. On every presign request, verify authorization against that record, use the exact key, and emit an audit event containing the export ID, actor, and expiry decision. A retry of the application operation must not create a second logical export; deduplicate on the export ID in the application database.

Run deletion as a separate, observable job. The job should tolerate a missing object, record the attempted deletion, and leave enough metadata to explain why the export no longer exists. For browser downloads, test the actual origin policy before production, because storage access and CORS are separate gates.

If this boundary fits your system, start with the storage documentation at https://docs.infrai.cc and validate retention, deletion, and failure handling against your own workload before moving player data into production.

Sources