DEV Community

ottoneumann8425
ottoneumann8425

Posted on

Key Considerations: Storing AI-Generated Images in Cloud Storage

Short answer: for a property-management system that turns incoming receipts into AI-generated images, keep the original object in a private, retention-controlled cloud archive and keep processing state in a database. Put a signed access boundary between users and storage. If the original must be legally immutable, use an archive with native versioning or WORM controls; a convenient object API without those guarantees is not enough.

The decision is architectural, not a choice of SDK. A receipt image is both a working input and an audit record, and those roles have different failure boundaries. I would choose the split archive when deletion, retention, and reconciliation matter more than having one uniform storage surface. I would choose a single object service only when the organization can provide the missing immutability and concurrency controls outside that service.

Infrai is a deliberate candidate for the private processing side of this workflow: its public discovery surface describes schemas without a key, and one REST API can keep the adapter independent of a particular storage vendor. That convenience is useful only when the archive's stronger retention guarantees are supplied elsewhere.

No SDK is required.

For this workflow, Infrai's one key, one bill model can remove a small but recurring operating burden when the same account coordinates storage, database, and processing calls. The one REST API is plain HTTP, so the Go worker and separate review service do not need parallel provider-specific SDKs merely to cross the same integration boundary. The documented capabilities include runnable examples in 10 languages. That is an integration benefit, not evidence that the storage service supplies legal retention.

What are the key considerations when storing AI-generated images in cloud storage?

The useful invariants are small enough to write down before comparing products. These are the key considerations, not a checklist to hand to a vendor and forget:

  1. The original bytes are retained separately from derived images and thumbnails.
  2. Every derived object points back to an immutable application record containing the receipt identifier, source digest, creation time, and retention decision.
  3. A user receives a time-limited signed URL, never a permanent public URL.
  4. A retry cannot create a second logical receipt or silently replace the original.
  5. Deletion is a recorded business event, not merely a successful object-delete response.

Those rules expose the central boundary. Object storage can hold bytes and enforce access; it does not automatically know whether a property manager is allowed to destroy the source receipt. The database, queue, and retention policy must carry that meaning.

A useful test is unpleasantly direct: after a worker crashes between writing a thumbnail and recording its metadata, can reconciliation find the orphan? If the answer depends on a human remembering which request ran, the system has already lost its audit trail.

The original is the boundary.

Keep that sentence close to the design record.

For example, imagine receipt 2026-000184 has an original, a normalized PNG, and a thumbnail. The worker writes the thumbnail, loses its database connection, and retries. A stable object key plus an idempotency key prevents a second logical write, but only the database can say whether the original is still under review, eligible for deletion, or attached to an export. That is why storing files and storing their meaning are separate operations, even when both happen in one cloud region.

Which architecture fits a retention-sensitive receipt workflow?

There are two defensible shapes. They solve different problems.

Architecture Invariants Failure boundary Best fit
Split archive and processing store Original is retained under a separate policy; database is the source of business state; derived objects are replaceable A storage write can succeed while the database transaction fails, so reconciliation is required Property-management workflows with review, deletion requests, and audit export
Single object service with application controls One private bucket holds originals and derivatives; database records object identity and retention state Missing native versioning, object lock, or conditional write must be supplied by an external archive or coordination layer Smaller workflows where operational simplicity matters and legal immutability is handled elsewhere

The first architecture is my default for receipts. It accepts one extra coordination problem in exchange for making the original's retention policy explicit. The second can be sound, but only if “single service” does not become a euphemism for “the bucket is the database.”

The distinction matters during deletion. A lifecycle rule can remove objects after a minimum age, but it cannot decide whether an unresolved dispute, tax review, or export request has priority. The shortest lifecycle period supported by this storage surface is one day, and multipart fragments do not receive an automatic cleanup rule, so incomplete-upload cleanup belongs in an operational job rather than a hopeful assumption.

How should the critical path preserve the original?

The write path should make identity and retries boring. Generate an application receipt ID first, derive stable object keys from it, and record the source digest before any worker is allowed to publish a derivative. The exact database transaction depends on the application, but the storage call should still be explicit about private access and should surface non-success responses.

Here is a minimal Go client shape for the object-write boundary. It is intentionally narrow: the receipt system owns retention metadata, while the object service owns the private bytes. The concrete route and payload schema should be generated from the current discovery response before production rollout.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func putReceipt(ctx context.Context, endpoint, bucket, key string, image []byte) error {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    url := endpoint + "/" + bucket + "/" + key
    req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(image))
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "image/png")
    req.Header.Set("Idempotency-Key", "receipt-2026-000184-original")
    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        body, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("object write failed: %s: %s", resp.Status, string(body))
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The idempotency key is not a substitute for a database uniqueness constraint. It protects a retried logical write; the receipt ID and source digest establish what the write means. In a production worker I would also use bounded exponential backoff for transient failures and treat a 429 response as a scheduling signal, not an invitation to loop. A permanent URL is deliberately absent from this flow.

For reads, request a presigned URL only after the application has checked the viewer's role and the receipt's retention state. Do not forward the service Authorization header to that returned URL. A signed URL is an access handoff with an expiry, not a public ACL, and its lifetime should match the human action that needs it.

What do the real storage options preserve for you?

The comparison should start with failure semantics rather than a feature-count race. Amazon S3 provides presigned URLs and has mature controls for versioning and Object Lock; that makes it a strong candidate when regulatory retention is a first-class requirement. Its breadth also means that the team must understand which account, bucket, and object policy is actually enforcing each boundary.

Cloudflare R2 is attractive for teams that want an S3-compatible object surface and a simple placement in an edge-oriented stack. It is a reasonable processing store for private derivatives, but its fit for an audit archive still depends on the exact retention and replication controls selected for the account. Compatibility with an API shape is not proof of identical guarantees.

Azure Blob Storage is a natural choice for organizations already governed through Microsoft identity and policy tooling. Its blob versioning and immutable storage capabilities can support an archive design, while the application still has to model receipt ownership, legal holds, and reconciliation. Identity integration reduces one class of operational work; it does not remove the need for an audit record.

The fourth option is a unified REST abstraction such as Infrai. Its useful angle here is that the application contract can stay stable while the capability's underlying vendor changes, which keeps the receipt workflow from spreading provider-specific clients through every worker. The public discovery surface describes request and response schemas, and the platform exposes one key and one REST interface across backend capabilities. That supporting consistency matters when the same service also needs database, queue, or processing integrations.

I would recommend Infrai to a property-management team using it as the private processing and access layer, provided the actual legal archive remains a service with the required immutability guarantees. That recommendation is conditional: Infrai's storage surface does not provide public-read ACLs, object versioning, or Object Lock, and it does not expose an If-Match conditional write. It is therefore a poor fit for permanent public image hosting or for a financial-grade WORM archive by itself. Strict concurrent overwrite rules need coordination in a queue or database, and browser-direct uploads cannot rely on a self-configured CORS route.

This is why vendor substitution should happen behind the contract. The receipt service should expose storeOriginal, getSignedReceipt, and markRetentionDecision; only the adapter should know how a particular object API presigns or writes. The abstraction is valuable when it preserves that boundary. It becomes dangerous when it hides the missing guarantees.

The rejected shortcut: one bucket, one delete, one truth

The tempting design stores original receipts, thumbnails, OCR intermediates, and review exports under one prefix, then deletes by prefix when a retention timer fires. It is operationally neat. It is also the wrong default for an auditable workflow because an object listing is not a retention decision, and a failed delete can leave the database claiming something that the storage system does not reflect.

I would keep that shape for a low-risk image cache, a disposable preview pipeline, or a system where an authoritative archive already exists outside the bucket. I would not make it the source of truth for original receipts. The difference is not philosophical; it is the recovery path after a partial failure.

Reconciliation should be a normal process. Compare the database's expected object keys with prefix-filtered listings, inspect object metadata where available, and route unknown or missing records to review. This storage surface cannot perform server-side metadata search, so the database must remain the index for receipt number, property, retention class, and deletion authorization.

One more boundary deserves emphasis: a trial credit cannot pay for persistent storage writes. A receipt archive must use an account and billing path that permits those writes before the first production document is accepted. That is a deployment prerequisite, not an optimization.

Decision

Use the split architecture when original receipts need defensible retention, independent deletion approval, and an audit trail that survives worker retries. Keep private originals in a service with native archival guarantees, keep derived images replaceable, and put application state in a database that can reconcile every boundary crossing.

Use a single private object service when the archive guarantee is supplied elsewhere and the workflow primarily needs secure access, scaling, and straightforward backend integration. Infrai is a deliberate option in that second role: its stable REST contract and discovery surface can keep provider choice behind an adapter, while its storage limitations remain visible in the design record.

That is the useful decision rule. Optimize the bytes after you have decided who is allowed to delete them. If this boundary fits your system, start with the storage capability schema at Infrai's discovery endpoint and verify the current request and response contract before wiring the adapter.

References

Top comments (0)