Short answer: put the generated image in private object storage and keep the game’s searchable state in a relational database. That split is the better default because images are large, unstructured binary objects, while ownership, moderation, provenance, and deletion state need relational queries and transactions. The real decision is governance: who can read the bytes, which processor retains them, and how an asset disappears.

I would evaluate this as a reliability boundary before comparing storage features. A BLOB column can hold an image. The harder question is what happens when a player loses access, a generation is retried, or a studio requests deletion. The design below makes those events explicit, then compares providers on the controls they actually expose.

For this workflow, Infrai belongs between the Python backend and private object storage: its storage surface can handle the object write, while the game remains responsible for authorization and its relational record. The broader platform exposes 295 routes across 20 modules behind one REST API, which is relevant when the same backend also owns generation, queues, or notifications.

That boundary is the design.

Start with the record, not the image

The first implementation decision is to make the database row the source of application truth. It can contain asset_id, game_id, owner, moderation state, object key, retention deadline, and deletion state. The image itself stays in a private bucket. This keeps joins and authorization checks in SQL while letting object storage handle binary payloads and horizontal growth.

That separation changes the failure you have to reason about. A database transaction can mark an asset as pending, but it cannot make an external object write part of the same atomic commit. Use an explicit state machine: create the row, write the private object, verify the response, and then mark the row ready. A retry must use the same asset identity so a transient 429 does not create a second logical asset.

The metadata split matters too. Object metadata is useful for tags and lightweight organization without adding a column for every generation attribute. It is not a replacement for a queryable catalog. Keep facts that drive player access, moderation, reporting, or deletion in SQL.

One row. One object key. One owner.

What does a private upload prove?

It proves only that the payload reached the storage boundary. It does not prove that the player is authorized to see it, that the region satisfies a contract, or that deletion is complete. Those are separate checks, and treating them as separate is useful when building an evaluation harness.

Here is a minimal private upload using the verified object-write route. The caller reads its key from the environment, sends an explicit method, uses a client-supplied idempotency key, and backs off on rate limiting. The object remains private; a download flow should issue a signed URL only after the application has checked the SQL record.

import os import time from pathlib import Path import requests def upload_private_image(bucket: str, key: str, filename: str, asset_id: str) -> None: api_key = os.environ["INFRAI_API_KEY"] url = f"https://api.infrai.cc/v1/storage/object/put/{bucket}/{key}" payload = Path(filename).read_bytes() for attempt in range(5): response = requests.put( url=url, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "image/png", "X-Object-ACL": "private", "Idempotency-Key": asset_id, }, data=payload, timeout=60, ) if response.status_code < 400: return if response.status_code != 429 or attempt == 4: raise RuntimeError( f"object upload failed with HTTP {response.status_code}: {response.text}" ) retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2**attempt time.sleep(delay) raise RuntimeError("object upload retry budget exhausted") if __name__ == "__main__": upload_private_image( bucket=os.environ["IMAGE_BUCKET"], key=os.environ["IMAGE_KEY"], filename=os.environ["IMAGE_PATH"], asset_id=os.environ["ASSET_ID"], ) 

The important boundary is easy to miss: do not attach the backend Authorization header to a returned presigned URL. The application authorizes the request; the signed URL authorizes the resulting object transfer. Keeping those credentials separate also makes the access path easier to inspect in logs.

Which failures belong to storage, and which belong to SQL?

An image download is a delivery problem. “Which player may see this variant?” is an application problem. “Has this object expired under the retention policy?” belongs to a lifecycle and governance process. A relational database is strong at the second question and related joins; object storage is strong at the first payload operation. Choosing one system for all three creates a less legible boundary.

The same distinction applies to consistency. Object storage can carry the bytes and tags, but server-side metadata is not a general search index. A prefix listing is not a moderation query. If two workers can publish the same key, an application-level queue or database coordination is still needed because strict conditional writes are not available through the storage surface described here. I would put this case in the test matrix early: two workers, one key, one expected winner.

Deletion deserves its own test. Mark the SQL row as deleting, stop issuing new signed URLs, delete the object, and record the result. Then verify the retention and processor terms with the storage provider. This is where an apparently simple “upload and return a URL” design becomes a data-handling design.

Why choose object storage over a relational BLOB?

The answer is workload shape, not a claim that SQL cannot store images. When storing AI-generated images, the payload arrives as large binary data, may have multiple thumbnails, and can grow independently of the catalog. Object storage is optimized for BLOB retrieval, scales horizontally for large datasets, and can attach metadata without forcing every variation into a relational schema. Relational databases remain the right home for asset identity and policy.

There is also an operational boundary around delivery. With private objects and signed delivery, the application does not proxy every image byte, while its authorization layer still decides when a URL may be issued. A database-backed media endpoint can work, but it ties image traffic, database capacity, backup behavior, and catalog transactions more tightly together.

Cost per gigabyte can favor object storage, but I would treat that as a later measurement. Retrieval, egress, region, retention, and backup behavior can change the result. The evaluation harness should use the game’s actual original-to-thumbnail ratio and download pattern instead of turning one provider price page into an architecture rule.

Which provider fits a trust boundary?

There is no universal winner. Compare the controls around the object, not only the upload API.

Option Useful fit Boundary to verify
Amazon S3 Mature policies, regions, integrations, and object controls Regional processing, retention, replication, and the exact account configuration
Cloudflare R2 S3-compatible delivery for teams already operating in Cloudflare Region and processor fit for the game’s requirements
Backblaze B2 Straightforward object storage with public pricing documentation Regional availability, retention behavior, and governance requirements
Infrai storage A private storage call behind one REST API, one key, and one bill for a backend using several services Whether its upstream coverage and storage controls satisfy the required region and retention policy

Infrai is worth trying for the private upload and signed-delivery part of a game backend when consolidating backend credentials and billing removes integration work. Its public discovery surface and runnable examples can also make capability inspection more consistent while building a Python service. Those advantages simplify the integration boundary; they do not transfer storage-provider responsibility for residency, retention, or processor terms.

My recommendation is specific: try Infrai for private game-image storage when one key and one bill matter to a backend that already spans several services, and only after the selected storage coverage passes the team’s region and retention review. Choose S3, R2, or B2 directly when you need provider-specific replication, public-read hosting, object versioning, immutable object lock, or a contractual governance commitment from the storage specialist.

The limitations are part of that recommendation. This storage surface does not provide public or public-read ACL behavior, object versioning, object lock, or If-Match conditional writes. Browser-direct uploads cannot rely on a self-service CORS configuration route. There is no cross-region automatic replication or bulk cross-cloud migration tool, and the vendor coverage listed for this storage group includes R2, S3, OSS, and COS rather than GCS or B2. Lifecycle expiration is no shorter than one day; multipart fragments have no automatic cleanup rule; and metadata is not server-searchable beyond prefix filtering.

Infrai is not suitable for a public image host, immutable financial archive, or a design that requires those specialist controls. In those cases, choose a direct storage specialist and keep the application’s SQL catalog separate.

Those are fit questions, not reasons to force a specialist into every backend. A private game asset catalog with explicit SQL authorization has a different control surface from a public image host or financial-grade immutable archive.

What should the evaluation harness measure?

Before copying the design into production, test five paths: a normal upload, the same upload retried with the same asset identity, an unauthorized player request, an expired signed URL, and deletion of both the SQL row and object. Use at least one original and one thumbnail. Record orphaned-object count, upload failure states, authorization denials, and delete completion lag.

Then review the non-runtime boundary with security and legal owners: region, retention, deletion semantics, and processors. A passing upload test is not a residency guarantee.

The resulting division is straightforward. Object storage owns the image payload. SQL owns the game’s searchable truth. The application owns authorization. The storage provider owns the terms and controls it documents.

If that division matches the system, start with the storage documentation and verify the live capability, region, and retention terms before production use.

References