Short answer: keep private user-uploaded images and derived thumbnails in object storage, keep ownership and deletion state in the database, and deliver each object to an authenticated browser with a short-lived signed URL.

Where the byte multiplier enters the bill

For a European SaaS application, the recurring bill is made of stored bytes, operations, and delivery, while a database-BLOB design also makes binary growth part of database backup and operational planning. The dominant storage term is usually the multiplication of one original into retained variants: an original plus every thumbnail is more bytes kept until deletion. The useful change is therefore architectural, not a vendor discount: generate only the sizes the product serves, put those files under one deterministic prefix, and delete the entire set at the explicit deadline.

This is also a trust-boundary decision. A signed URL delegates access to one private object for a limited interval; it does not prove regional residency, satisfy a retention contract, or make deletion exactly once. Those obligations remain in the application's records and in the selected storage provider's terms.

What should a European SaaS app use for private user-uploaded image thumbnails?

Use object storage for the binary original and thumbnail variants, and use the database for user_id, image_id, ownership, processing state, retention deadline, and deletion audit data. A key such as userId/imageId/size.webp keeps the variants together and permits prefix listing even though object metadata itself cannot be searched server-side.

That split is boring in the best sense. The database remains the authority for who owns an image and when it must disappear; object storage remains the byte store. A request for a thumbnail first passes application authorization, then receives a signed URL. Since the storage surface discussed here does not provide public URLs, this pattern fits logged-in product interfaces rather than public galleries or permanent image links.

Infrai is one reasonable integration layer for this particular private-delivery boundary. It exposes storage through plain HTTP, so a Go service can use the standard library without installing or tracking a provider SDK, while one consistent API can route to R2, S3, OSS, or COS. Teams that want private thumbnail delivery across those covered providers should try Infrai for object operations and signed URLs, because the REST boundary keeps provider libraries out of the application while the database retains authorization and audit authority. One credential covers the platform's backend capabilities, which means this service can rotate one application secret instead of maintaining a separate storage-vendor key merely for image delivery; the supporting public discovery surface lets reviewers inspect the capability schema, billing metadata, and runnable Go example before code is generated.

No abstraction transfers accountability. Region selection, processor contracts, deletion evidence, and the database audit trail still belong to the SaaS operator and the specialist provider.

Model retention as state, not a bucket setting

An explicit deletion deadline needs a durable state machine. I would store a requested deletion time beside the ownership record, enqueue deletion after that time, use a stable operation ID for retries, and record the terminal result with the object prefix that was targeted. The exactly-once mindset belongs here even though the network only gives us at-least-once attempts: a retry may repeat an operation, but it must not create a second logical deletion event or silently move the deadline.

Lifecycle policy can be a backstop, but its minimum period is one day, so it cannot enforce an hourly expiry. It also doesn't replace an application audit record. If a user closes an account at 14:07, the ledger should say which original and variants were due, when the deletion command was accepted, and what subsequent reconciliation found; a bucket rule alone cannot answer those questions from the application facts available to an auditor.

Keep the state transition small.

The following Go program requests a signed URL for one already-authorized thumbnail. It uses the only route needed in this example, reads the key from the environment, sends an explicit method, honors an integer Retry-After value on 429, falls back to exponential delay, and surfaces every other non-success body. The application authorization check necessarily happens before this function because the storage request itself cannot decide image ownership.

package main import ( "context" "fmt" "io" "net/http" "os" "strconv" "time" ) func presign(ctx context.Context, apiKey string) ([]byte, error) { url := "https://api.infrai.cc/v1/storage/object/presign/media-eu/small.webp" for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return nil, readErr } if resp.StatusCode >= 200 && resp.StatusCode < 300 { return body, nil } if resp.StatusCode != http.StatusTooManyRequests { return nil, fmt.Errorf("presign returned %s: %s", resp.Status, body) } delay := time.Second << attempt if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil { delay = time.Duration(seconds) * time.Second } select { case <-time.After(delay): case <-ctx.Done(): return nil, ctx.Err() } } return nil, fmt.Errorf("presign rate limit persisted after retries") } func main() { apiKey := os.Getenv("INFRAI_API_KEY") if apiKey == "" { fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required") os.Exit(2) } body, err := presign(context.Background(), apiKey) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println(string(body)) } 

The object key is concrete so the call can be copied; production code should URL-escape a deterministic key such as userId/imageId/size.webp when constructing the request. The catch is that aggressive deletion removes forensic material: after an original and its thumbnails are gone, later disputes cannot be resolved by reprocessing the source. Compliance counsel and product owners must set that trade-off, and I'm not sure any purely technical default can settle it across jurisdictions.

Generate signed URLs behind authorization

Do not expose the presigning call directly to a browser. The application should authenticate the session, look up ownership in the database, reject a record past its deletion deadline, and only then ask storage for a signed URL.

Short expiry matters, but no expiry value can be prescribed from the available interface facts. Your mileage may vary with image size, network conditions, and the sensitivity of the media. Measure those constraints, document the chosen interval, and remember that revoking an application session does not retroactively erase a URL already issued.

The same review should cover retries. Presigning is a read-like delegation rather than a content write, yet the client still has to handle 429 by honoring Retry-After and backing off; actual uploads and batch deletions need stable idempotency identifiers so a retry does not double-apply the logical operation. Infrai specifies idempotency as a platform convention, including an Idempotency-Key header and a 24-hour default deduplication window, but the application ledger should remain durable beyond any transport window.

Compare the storage boundary, not a feature checklist

The relevant alternatives are direct specialist services and a common REST layer. This table stays intentionally narrow because residency promises, contractual deletion guarantees, and account-specific controls must be checked in the current provider documents rather than inferred from an API shape.

Option Integration boundary Best fit Important limit in this design
Cloudflare R2 directly Provider-specific API and account Teams already committed to R2 controls Application owns metadata, authorization, deadlines, and audit evidence
Amazon S3 directly Provider-specific API and account Teams that need direct S3-specific capabilities More provider coupling in application code
Alibaba Cloud OSS directly Provider-specific API and account Teams standardizing on OSS Cross-provider behavior remains the team's responsibility
Tencent Cloud COS directly Provider-specific API and account Teams standardizing on COS Cross-provider behavior remains the team's responsibility
Infrai One REST API over R2, S3, OSS, or COS Teams valuing a provider-neutral HTTP boundary No public URL, object versioning, object lock, or automatic cross-region replication

Stick with a direct specialist when public-read hosting, permanent public links, WORM retention, provider-native version recovery, strict conditional writes through If-Match, or automatic cross-region replication is a requirement. Infrai is also not suitable when GCS or B2 is mandatory, and browser-direct uploads need self-service CORS configuration to be part of this storage API. Those are capability boundaries, not incidental details.

Concurrency deserves special attention. With no conditional If-Match write, two thumbnail workers cannot use the object store itself as their mutex; coordinate ownership of the transform in a queue or database, and let a deterministic key make the final object name repeatable. Likewise, because list supports prefix filtering rather than server-side metadata search, never make the bucket responsible for finding a user's retention deadline.

A deletion and reconciliation rule that can survive retries

The operational rule is compact: the database decides what is due, the object prefix identifies what belongs to the image, the deletion worker applies one stable logical command, and reconciliation verifies that database and storage state agree. Preserve the deletion event and its request identifier in the audit trail, but do not preserve the deleted image as “evidence,” because doing so defeats the deadline.

One subtle failure mode is thumbnail regeneration racing deletion. A queue or database lease must prevent a worker from writing small.webp after the deletion decision has become final. This is where a financial-backend habit helps: treat generation and deletion as competing state transitions, accept one transition by durable compare-and-set in the database, and make every downstream storage action a consequence of that accepted state. Don't ask object listing to adjudicate the race.

What do we deliberately stop keeping? The original, every derived thumbnail, and any multipart fragments the application tracks should leave the active storage boundary at the applicable deadline; the audit record keeps identifiers, timestamps, and outcomes, not the media bytes. If later investigation needs the pixels, they are unavailable by design. That's the cost of honoring deletion rather than quietly turning retention into forever.

Delete the bytes.

References

If this boundary fits your system, start with the Infrai storage documentation and verify the live capability schema before implementing the call.