Short answer: for an app choosing an object storage service for data backup, use direct object delivery when expiry is an acceptable revocation model; choose an authenticated proxy when every download needs a current authorization decision.
A media application serving generated reports to authenticated customers has two different problems: keeping backup objects through their retention window, and deciding who may retrieve a report today. I would choose signed object delivery when the object store can enforce the required lifetime and the customer can fetch directly. I would choose an authenticated proxy when every download needs a server-side authorization decision, redaction, or audit record. The storage service is a secondary decision.
The useful test is not which option has the nicest upload API. It is whether a leaked download link is an acceptable failure. I initially treated backup storage and customer delivery as one design choice. That made the retention rule carry too much responsibility. For app-data backups, I split the design into three boundaries: operational recovery, customer delivery, and deletion. That separation keeps lifecycle automation from quietly becoming an access-control policy.
Which object storage service should handle app data backup?
For generated media reports, a backup object may contain a customer identifier, a time range, and derived metrics. A customer session proves identity to the application; it does not automatically make an object URL safe to share. A signed delivery link moves the final byte transfer away from the application, which is attractive for large files and bursty demand. Its weakness is also clear: possession of the link is usually the practical bearer credential until it expires or is revoked by the surrounding design.
An authenticated proxy keeps the authorization check in the application path. The proxy can verify the session, check tenant ownership, record the report identifier, and stream only after those checks pass. This gives a narrower control point, but it makes bandwidth, timeout handling, and range requests part of the application’s operational surface. A proxy is not automatically safer; it is safer only when its checks are complete and its logs do not leak the same object credential.
That is the first boundary. Backup retention should not decide customer authorization.
Which boundary changes the choice?
| Boundary | Signed object delivery | Authenticated proxy |
|---|---|---|
| Large report transfer | Fewer application bytes and less connection work | More control, more application capacity required |
| Per-request authorization | Usually expressed before issuing the link | Evaluated on every request |
| Immediate revocation | Needs a short expiry or an external deny list | Natural fit for session and tenant checks |
| Audit detail | Must be captured when issuing and using the link | Can be captured at the download boundary |
| Browser integration | Direct download can require CORS and careful URL handling | Same-origin delivery can simplify browser policy |
The table is a trade-off map, not a ranking. If a customer can download a report for 15 minutes after an administrator removes access, a signed link may fail the product requirement. If a report is 2 GB and customers download it in parallel, forcing every byte through a Python process may fail a capacity requirement. Measure those constraints before copying the pattern.
How do lifecycle rules interact with recovery?
Lifecycle automation is a retention mechanism, not a backup strategy. A rule that expires old objects can be correct for temporary exports and disastrous for the only recoverable copy of application data. The retention clock also needs an explicit event: object creation, backup completion, or a verified copy into another recovery boundary. “Older than 30 days” is meaningless until the team agrees what age represents.
I use separate prefixes or metadata for backup classes, then test each class with a restore job. The exact mechanism varies by storage service, but the invariant is portable: a customer report should not share a deletion rule with a recovery artifact merely because both are binary objects. Lifecycle systems commonly support age-based transitions or expiration, and the rule must be reviewed alongside the restore objective, legal hold requirements, and failure recovery procedure.
A small Python policy function makes the intent visible to an eval harness. It is deliberately independent of a provider SDK.
from datetime import datetime, timedelta, timezone def retention_deadline(created_at: datetime, kind: str) -> datetime: periods = { 'customer_report': timedelta(days=30), 'daily_backup': timedelta(days=35), 'monthly_backup': timedelta(days=400), } if kind not in periods: raise ValueError(f'unknown backup class: {kind}') if created_at.tzinfo is None: raise ValueError('created_at must include a timezone') return created_at + periods[kind] now = datetime.now(timezone.utc) assert retention_deadline(now, 'daily_backup') > now The code does not delete anything. That is intentional. A deletion worker should consume an approved policy, emit an audit event, and have a dry-run mode. Before enabling expiration, I would run a restore evaluation against objects near each deadline, including an object whose name contains spaces or percent characters. URI percent-encoding rules matter when an object identifier crosses from a database into a URL; treating a decoded path as the canonical key can select the wrong object.
Where does the browser become part of storage?
Direct delivery puts browser behavior on the decision list. A web application making a cross-origin request may need the storage origin to return the appropriate CORS headers. CORS is a browser enforcement mechanism, not an authorization layer, so allowing an origin does not establish that the requesting customer owns a report. The application still needs to decide who receives a link and for how long.
URL construction deserves the same care. Object keys, query parameters, and signatures have different encoding rules; a space, slash, or percent sign can change the bytes being signed or the resource being addressed. I would test the exact canonical string, not only a happy-path filename such as report.pdf. This is a small test with a high failure yield.
A proxy reduces some browser policy friction, but it does not remove HTTP concerns. It still needs bounded response sizes, timeout behavior, range-request decisions, and a clear distinction between a missing object and an unauthorized object. Those responses should be evaluated as security behavior, not just as frontend error states.
What should be measured before adoption?
Start with an evaluation set, because notebook-to-prod assumptions are especially unreliable around access boundaries. I would include at least these cases: an active customer requesting its own report, a customer requesting another tenant’s identifier, a link replay after expiry, an administrator revoking access during a download, a report just past its retention deadline, and a key containing reserved URI characters. Record the expected authorization result, audit event, bytes transferred, and cleanup decision for each case.
Then measure the real workload: report size distribution, concurrent downloads, time-to-first-byte, proxy CPU and memory, lifecycle evaluation lag, restore duration, and the number of authorization checks per report. Track token or prompt cost separately if an AI system generates the reports; generated text cost should not influence a retention rule. A cheap report that cannot be restored is still a failed backup.
My default decision rule is concise. Use signed object delivery when expiry is an acceptable revocation model, direct transfer matters, and issuance logs are enough for the audit requirement. Use an authenticated proxy when authorization can change during a download, every access needs a current policy check, or the response must be transformed. In either case, keep backup lifecycle classes separate from customer access classes and test deletion as seriously as restoration.
There is no universal best storage service for this workflow. The better choice is the service and delivery path that make the three boundaries observable, testable, and boring under recovery pressure.
Sources
References:
- MDN, Cross-Origin Resource Sharing (CORS): https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- AWS documentation, Object lifecycle management: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
- RFC 3986, URI Generic Syntax: https://www.rfc-editor.org/rfc/rfc3986
