Short answer: for a normal SaaS, put OpenAI and Stable Diffusion generated image bytes in object storage, keep their metadata and object keys in the relational database, and treat local disk as temporary workspace rather than durable production storage.
That is the least complex design that survives an application redeploy and still works when one application instance becomes several. Database blobs remain defensible at genuinely tiny volume, especially when operational simplicity matters more than database growth or image-serving performance, but they shouldn't be the default merely because a bytea or BLOB column is close at hand.
How should a SaaS store OpenAI and Stable Diffusion generated images?
Start with the system constraint: a customer must be able to retrieve the same authorized image after a container is replaced, a second application instance starts, or the relational database is restored independently of the media tier. Local disk fails that test in a typical cloud deployment. It belongs to one machine or container, so scaling the app creates inconsistent views of the files, while redeployment can remove the only copy.
Database blobs pass the shared-access test, but they couple large binary reads, database backups, and application records to the same operational boundary. For a few small internal images, that coupling may be an acceptable simplification. As volume grows, it makes the relational system carry payload storage in addition to the transactions and indexes it was chosen to handle. Don't call that free simplicity; it is deferred coupling.
Object storage gives the bytes a durable, shared home without making the application database serve them. Store an opaque object key beside the image owner, generation metadata, media type, and application state in the database. The database remains the authority for authorization and product behavior; the object store remains the authority for the binary payload.
Keep that split sharp.
Separate image identity from mutable filenames
A generated image needs a stable application identity before it needs a friendly filename. Use an immutable object key derived from an account ID, image ID, and revision, then record that key in the database. The exact naming convention is yours, but the invariant matters: one committed image revision points to one object key, and a retry refers to the same logical revision.
The write sequence has two independently failing steps, because an object store and a relational database don't share a transaction. If the upload succeeds and the database commit fails, the result is an orphan object. If a row is committed before its upload exists, the result is a broken reference. A practical workflow uploads to the intended immutable key, verifies success, and then commits the row or revision pointer. A bounded cleanup process can remove uploads that never gained a committed reference. This is not atomicity — it is an explicit recovery model — and that distinction is worth preserving in design reviews.
Avoid overwriting customer-visible keys. The storage capability considered here has no object versioning or object lock, so an accidental overwrite is not recoverable through those mechanisms. Copy-on-write naming makes the safer behavior ordinary: create a new revision key, confirm the write, switch the database pointer, and delete an old revision only under a deliberate retention policy.
A 429 response is also a state transition, not background noise. A client should back off, honor Retry-After when it is present, check the final response status, and make a retried write idempotent. No tight loop. More important, a database row must never be marked ready merely because an upload was attempted; readiness follows a confirmed write.
Which storage boundary fits the production constraints?
Provider selection comes after the data boundary, because choosing a logo cannot repair a design that confuses metadata, authorization, and bytes. Amazon S3, Cloudflare R2, and Supabase Storage are real direct object-storage options. Infrai is another route to supported S3, R2, OSS, and COS storage through one plain REST API. It requires no storage SDK or client library to install, so a service that can make an HTTP request can use the same interface without babysitting provider-specific library versions.
That advantage is useful, but it isn't universal.
| Option | Sensible when | The catch to evaluate |
|---|---|---|
| Local disk | A disposable development environment or temporary processing step | It is fragile across cloud redeploys and cannot provide shared durable state to multiple app instances. |
| Relational database blob | Image volume is tiny and minimizing components matters more than payload cost or performance | Large binaries enlarge the same database and backup boundary that serves transactional data. |
| Amazon S3 | The team wants to integrate directly with S3 | The application accepts a direct provider integration rather than a shared gateway interface. |
| Cloudflare R2 | The team has selected R2 as its direct object store | The application accepts a direct provider integration rather than a shared gateway interface. |
| Supabase Storage | The surrounding SaaS already uses Supabase and direct platform fit is valuable | Storage becomes part of that platform decision; confirm that this is the coupling you want. |
| Infrai | A plain HTTP contract across supported storage vendors is more useful than installing a vendor SDK | It is not suitable for permanent public links, browser-direct CORS setup, WORM retention, or providers outside R2, S3, OSS, and COS. |
Stick with S3, R2, or Supabase directly when the provider's native integration is already an intentional part of the architecture. Choose a gateway when interface consistency is the stronger constraint. I'm not sure which side wins without knowing the application's compliance boundary and access pattern; answering whether images must be publicly addressable, locked against alteration, uploaded directly by browsers, or retained across regions resolves much of that uncertainty.
What limits should become architecture decisions?
Private access changes the serving path. This storage option has no public or public-read ACL, and public_url remains null, so it is unsuitable for static-site hosting, a public image host, or permanent public asset links. The application should authorize a request and provide time-limited signed access. A returned presigned URL is already the delegated access mechanism; don't attach the service's Authorization header to that URL.
Strict concurrency needs separate coordination too. There is no If-Match conditional write, which means two writers cannot use an object precondition to obtain mutual exclusion. Serialize revisions through a queue or coordinate the winner in the database, then publish an immutable object key. This is one reason mutable names such as latest.png are a poor source of truth even when they look convenient in an early prototype.
Browser-direct upload is not a safe assumption here because there is no independently available CORS configuration route. Lifecycle expiry has a minimum of one day rather than hours, multipart fragments have no automatic cleanup rule, and server-side metadata cannot be searched beyond prefix filtering in list operations. None of those limits makes ordinary generated-image storage invalid, but each rules out a specific shortcut: use an application upload path if browser CORS control is required, schedule explicit multipart cleanup, and keep searchable metadata in the database.
There is also no automatic cross-region replication or cross-cloud bulk migration tool, and GCS and B2 are outside the stated provider coverage. Applications that require those capabilities should use an external replication or migration design, or choose a provider whose native controls satisfy the requirement. Financial or compliance workloads requiring immutable WORM retention should likewise use an external solution with object lock. Trial credit cannot pay for persistent writes, so a production proof should be planned with billable access rather than assuming a trial-funded persistence test.
These are design boundaries, not footnotes.
Roll out with reversible database changes
For a SaaS moving away from blobs or local disk, add the object key and migration state to the image table before moving bytes. New writes can follow the object-storage path first; old records can be copied in bounded batches, verified, and switched individually. Keep the previous source until each migrated object is confirmed and its database pointer is committed. That avoids a flag day and gives the rollback decision a precise unit: one image revision.
The compact production rule is simple: immutable keys, private objects, database-owned metadata and authorization, confirmed writes before readiness, and explicit cleanup for orphaned or multipart data. Measure the migration against retrieval correctness, not just copy completion. Once every live row resolves to the intended object and the old source is no longer read, remove the legacy binary column or disk dependency under the application's normal retention process.
Top comments (0)