# AI Image App Retention: Private Storage, Signed Downloads, and Public-Bucket Alternatives

> Source: <https://dev.to/griffinhayes3461/ai-image-app-retention-private-storage-signed-downloads-and-public-bucket-alternatives-30l2>
> Published: 2026-10-22 22:09:51+00:00

**Short answer:** For a property-management AI app that stores signed documents or generated images until a stated deletion deadline, use private object storage with application-controlled authorization and short-lived signed download links; keep the bucket private, store the retention deadline beside the object record, and make deletion a tracked state transition.

That's the rule.

No magic.

The least complicated design is private object storage with an application-controlled authorization step. It keeps the object closed while the application decides who may receive a temporary link, and it avoids making a permanent URL part of the document's identity.

The word “cheapest” is too small a question to carry this decision. A public bucket may remove one delivery step, but it turns a document-retention problem into an access-control problem that every URL, cache, and browser now participates in. A private bucket costs some control-plane work. That work is usually the cheaper risk.

Start with an object record, not a URL. For a signed lease document, the database might hold a generated object key, tenant or property identifier, document status, created time, deletion deadline, and a checksum or size captured after upload. The object store holds bytes; the database answers questions about ownership, legal state, and what should happen next.

The request path is deliberately boring:

That separation matters. A link is a bearer capability: anyone who obtains it may be able to use it until it expires, so it belongs in the same threat model as a password reset link, even though its lifetime is shorter. Do not put a permanent object URL in a lease table, email template, or audit event and call the object private afterward.

The expiry should match the action. A small document on a good connection needs less time than a large AI-generated image on a mobile connection. I'm not sure there is a universal duration that is defensible across both cases; measure download completion and rejected-link rates, then choose a product-specific window. A signed link also cannot revoke a copy that a reader already downloaded.

The delivery layer should set cache behavior intentionally. `Cache-Control` response directives control how caches may store and reuse a response; they do not replace authorization at the origin. For sensitive documents, choose directives that match the exposure you accept, and test an actual browser plus any CDN or reverse proxy in front of the download path.

Retention has two separate clocks. The first is the product clock: “delete this signed lease seven years after the event,” or “remove this generated image after the user deletes the property.” The second is the storage clock: how quickly a lifecycle rule or cleanup worker notices and removes the bytes. A policy can be correct on paper while the application still serves an object because its database state was never updated, or because a cleanup job never observed the deadline.

Represent those states explicitly. `active`, `pending_deletion`, `deleted`, and `deletion_failed` are more useful than a single nullable URL. A worker can claim due records, attempt deletion, record the provider response, and retry with backoff. Once deletion succeeds, the download endpoint should stop minting new links even if a stale link has not yet expired.

Here is a provider-neutral sketch. The storage client is an interface on purpose: the policy is the part worth testing, while the SDK or HTTP adapter belongs at the edge of the system.

``` python
from datetime import datetime, timezone

def issue_download_link(user, document, storage, now=None):
    now = now or datetime.now(timezone.utc)

    if document.status != "active":
        raise PermissionError("document is not available")
    if document.deletion_deadline <= now:
        raise PermissionError("document has reached its deletion deadline")
    if not user.can_read_property(document.property_id):
        raise PermissionError("property access denied")

    return storage.presign_get(
        key=document.object_key,
        expires_in_seconds=300,
        response_cache_control="private, no-store",
    )
```

The five-minute value is an example of a testable policy, not a universal security recommendation. The adapter must also ensure that the signed URL is used as returned; the application credential should not be copied into the browser request. If the response is intended to be cached privately, use a cache policy that says so; if it must not be retained by a cache, make that explicit and verify the response headers.

Deletion is where many apparently inexpensive designs become expensive. A job that marks a database row deleted but leaves the object behind creates retained bytes with no owner. A job that deletes bytes first and commits application state later can leave a record that appears downloadable but has nothing to serve. Use a claim-and-record workflow, make the operation idempotent, and reconcile records against storage inventory on a schedule that matches the deadline's operational promise.

Public delivery is appropriate for some products: an open gallery, an image intended for anonymous embedding, or a static asset whose URL is genuinely public. It is not suitable for signed leases, tenant documents, moderation material, or AI outputs whose viewer must be checked. “The URL is hard to guess” is not an access-control policy.

The trade-off is clearer when the choices are written down.

| Design | Access control | Delivery simplicity | Retention work | Use it when | 
|---|---|---|---|---|
| Private object storage plus signed GET | Application checks every link request; the link expires | Moderate; one authorization call precedes the download | Explicit database state and cleanup reconciliation | Documents or images belong to a tenant or user | 
| Private storage plus application streaming | Application stays in the request path | Familiar to clients; more bytes cross your service | Cleanup is still explicit | You need a single response policy or extra inspection | 
| Public bucket or public asset host | URL possession is usually the practical permission | Simple for anonymous readers and embeds | Deletion must account for copied and cached URLs | The asset is intentionally public | 
| Self-hosted object layer | Policies are yours to operate and audit | Depends on your deployment | You own replication, backups, upgrades, and cleanup | Control or locality outweighs operational burden | 

The cheap-looking option often shifts work rather than removing it. With a public bucket, revocation becomes difficult because clients can cache or copy the URL. With application streaming, bandwidth, timeouts, and range requests become your service's concern. With self-hosting, durability and recovery are team responsibilities, not checkboxes on a storage form.

The catch is that private signed delivery is not a fit for permanent, anonymous web embedding. Pick a public delivery design when that is the actual product requirement. Conversely, don't accept a public bucket merely because a frontend component wants an `<img src>` that never changes; the frontend can request a fresh signed URL or receive the bytes through an authorized endpoint.

Test the boring failures first. A useful test matrix includes an expired link, a user from another property, a document whose deletion deadline is due, a duplicate cleanup claim, a retry after a transient provider failure, and a browser that starts downloading slowly. Also test a stale database row pointing to a deleted object and an object with no database owner. Those cases are where the access-control and retention contracts meet.

Browser upload behavior deserves its own boundary. If the browser uploads directly to private storage, the application must issue an upload authorization scoped to the intended object and still validate the resulting record. If the application receives the upload first, it gets simpler policy enforcement but takes on the transfer path. MDN's XMLHttpRequest documentation describes upload progress events, which can help a client distinguish a slow transfer from a failed request; progress UI does not prove that the server accepted the right bytes.

Object keys should be immutable and unguessable enough not to become a second identity system. Put searchable fields such as property, tenant, status, and deletion deadline in the database. A prefix can help operational listing, but it is not a query engine and it is not authorization.

Observability should answer four questions for every document: who requested access, which policy decision was made, which object key was involved, and whether deletion reached a terminal state. Log identifiers and outcomes, not signed URLs. A URL in logs, analytics, exception reports, or support screenshots is a credential leak with a time limit.

Build the authorization path before adding convenience URLs. In a staging bucket, create a document, verify that anonymous access is denied, mint a signed download link for an authorized user, reject the same request for a different property, and confirm that the link expires. Then set a short test deadline and observe the complete deletion transition.

Keep the provider adapter narrow. It needs operations such as put, signed GET, delete, and a way to inspect enough metadata for reconciliation; the rest of the application should depend on a policy-facing interface. This makes alternatives comparable without pretending their durability, lifecycle, replication, or billing models are identical. Your mileage may vary when a provider adds CDN caching, archival tiers, or regional constraints, so record those as explicit decision inputs.

At launch, alert on due records that remain active, deletion failures older than the retry budget, and storage growth that cannot be explained by active database rows. A daily report is not a retention guarantee by itself, but it is a useful control for finding drift. For a strict deadline, the cleanup worker, retry policy, clock source, and reconciliation cadence belong in the same operational document as the storage choice.

The decision rule is short: private object storage with expiring signed downloads is the default for property documents and user-owned AI images; public delivery is for assets that are intentionally public. Keep the retention deadline in application state, keep credentials out of browsers and logs, and test deletion as seriously as download.

## Also reported by

- dev.to: Private Image Storage: Signed Download Links, Retention, and Object-Store Trade-offs <https://dev.to/owensullivan9135/private-image-storage-signed-download-links-retention-and-object-store-trade-offs-461i>
