AI Image App Retention: Private Storage, Signed Downloads, and Public-Bucket Alternatives A developer outlined a provider-neutral design for handling AI-generated images and signed documents in a property-management app: keep objects in private storage, gate access through application-controlled authorization, and issue short-lived signed download links rather than exposing public buckets. The approach stores a retention deadline alongside each object record and models deletion as explicit states (active, pending_deletion, deleted, deletion_failed) so a cleanup worker can claim, retry, and confirm removal. The author argues the extra control-plane work is cheaper than turning a retention problem into an access-control problem spread across every URL, cache, and browser. 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