Short answer: for a property-management SaaS, choose storage by the deletion contract and access-control boundary first, then compare lifecycle behavior and total operating cost; a lifecycle rule can clean up expired files, but it is not a substitute for legal hold, recovery, or an authorization design.
That distinction is the decision rule. Signed leases and inspection reports are user documents, but they are not all the same operational object. A file waiting for an application review can be private, short-lived, and easy to recreate. A signed lease may need a stated deletion deadline, a documented access path, and a recovery plan that survives an accidental overwrite.
The system should make those differences visible. Otherwise a bucket policy quietly becomes the retention policy, and the team discovers the mismatch during an audit or an incident.
The incident lesson is to separate cleanup from custody
I use a bounded scenario in design reviews: a property manager uploads a signed lease, the tenant can view it, an internal service extracts metadata, and the platform must delete the file after the contractually defined deadline. A second copy may be needed for disaster recovery, but it must not become an excuse to retain the customer document forever. This is a model for testing the design, not a claim about a particular production event. In the review, I trace the object from upload to quarantine, from quarantine to an authorized download, and from the deadline to the deletion audit event. Then I deliberately replay the awkward cases: the browser retries after a timeout, the metadata write arrives after the object write, an employee loses access while a download is in progress, and a legal hold is placed just before the cleanup worker runs. If the design cannot say which state wins in each case, the lifecycle rule is doing work that belongs in an application state machine.
The invariant is simple: the component that decides who may read a document should not be the only component that decides when the document disappears. Application metadata can hold the retention deadline and the legal state; object storage can enforce the coarse cleanup rule; a scheduled reconciliation job can find objects whose metadata and storage state disagree.
That separation gives the on-call engineer something concrete to inspect. The record should include an object key, tenant identifier, classification, deletion deadline, and current authorization state. The object key should be treated as an opaque identifier rather than a place to expose a tenant name or an address. OWASP recommends storing uploaded files outside the webroot, applying authorization checks, and validating the upload rather than trusting its filename or content type. Those controls matter before lifecycle cleanup enters the picture.
The failure mode is usually mundane. A user changes the filename, a retry creates a second key, or a deleted database row leaves an object behind. A lifecycle rule cannot infer that two keys represent one lease. It needs a deliberately chosen prefix, tag, or other provider-supported selector, and the application still needs a reconciliation path for exceptions. It is easy to miss one of these paths because the happy case has only one object and one deadline; the audit case has an object, a record, an authorization decision, a hold state, and an operator who needs to explain why deletion did or did not happen.
Short version: deletion is a workflow.
How should SaaS teams compare lifecycle cleanup for archived documents?
Start with the questions that become acceptance tests, not with a price table. For S3 lifecycle, R2 lifecycle, and Wasabi lifecycle, verify the current documentation and contract for the exact behaviors your archive requires. The names describe comparison candidates; they do not establish that the candidates expose identical retention, versioning, replication, restore, or access-control semantics. Don't treat a matching feature label as matching behavior: “lifecycle” can describe a cleanup mechanism while leaving the timing, selectors, recovery model, and authorization path to separate documentation.
| Question | Why it matters for signed documents | Evidence to require |
|---|---|---|
| What selects an object for cleanup? | A broad prefix can delete the wrong tenant's files; a narrow rule can leave retries behind. | A tested rule using the actual key and metadata scheme |
| When does deletion become effective? | The application may promise a deadline while the storage service applies lifecycle work asynchronously. | Provider behavior, timestamps, and an observed test |
| Can an authorized reader retrieve the file without making it public? | A lease is private customer data, not a static asset. | A private-bucket test and an authorization trace |
| What survives an overwrite or accidental deletion? | A backup requirement is about recovery, not merely retaining bytes. | Version, replication, and restore documentation plus a drill |
| Who owns exceptions? | Legal hold, dispute, failed deletion, and migration all need an owner and an SLO. | Runbook, alert, escalation path, and audit record |
This is where “cheap” becomes a weak primary axis. Capacity planning needs object count, average and tail size, upload rate, retention distribution, read traffic, and restore traffic. The bill is only one input; engineering time, reconciliation jobs, egress during recovery, and pages caused by a missed deletion deadline also belong in the estimate. I would also model the retention distribution by document class instead of multiplying today's average bytes by a guessed number of months: signed leases, generated exports, thumbnails, quarantine objects, and recovery copies have different lifetimes and different read patterns. That model makes a policy change reviewable. If the product team changes a default deadline, the platform team can see which object classes grow, which deletion tests need to change, and which restore assumptions no longer hold.
I would put each candidate through the same small test matrix. Upload a private sample, grant access to the intended tenant principal, deny access to a different tenant, schedule a disposable object for expiry, and confirm the observed deletion behavior. Then overwrite a sample, attempt a restore, and run the reconciliation check. Record the provider, region, policy revision, timestamps, and result. Your mileage may vary across account settings and regions, so a marketing comparison is not evidence for an SLO.
The access path is part of the retention design
The safest delivery path for a browser is usually an application authorization decision followed by a narrowly scoped, short-lived download capability. The browser should not receive a long-lived storage credential. The service should check tenant membership and document state before issuing access, and logs should record the decision without logging the document contents or secrets.
Uploads need the same discipline. Generate the storage key server-side, constrain size and accepted formats, validate the file signature where appropriate, and scan or quarantine content before it becomes available to other users. OWASP's file-upload guidance is a useful baseline because an upload endpoint is an input boundary, not just a convenient pipe into a bucket.
For signed leases, the database transaction and object write also need an explicit relationship. A practical sequence is to create a pending document record, upload to a private quarantine key, validate the object, promote it to its immutable application identity, and then mark the record available. A retry must be idempotent. If the upload succeeds but the transaction does not, reconciliation should either complete the record or delete the orphan according to a recorded deadline.
RFC 3986 is relevant in a less glamorous place: object keys and signed URLs cross URI boundaries. Percent-encoding is not decoration. A key containing spaces, reserved characters, or non-ASCII input must be encoded according to the URI component being built, and the exact canonical form expected by the signing scheme must be preserved. Treating a raw user filename as a URL path is how an apparently valid link turns into the wrong object or a signature mismatch.
What should the buy-versus-build boundary be for document archives?
Buy the storage primitive when it supplies the documented durability, private access, lifecycle selectors, and regional behavior that the retention SLO needs. Build the application controls around it: tenant authorization, metadata, deletion deadlines, idempotency, reconciliation, audit events, and restore tests. That division keeps the storage service from becoming a second application database while avoiding a homegrown object store.
Build additional coordination only when the requirement is explicit. A database or queue can own fine-grained deadlines, retries, and legal-state transitions; an object lifecycle rule can provide a backstop for ordinary expiry. If the requirement is immutable retention or a legally enforceable hold, select a storage capability whose current contract supports that control and have the compliance owner verify it. Do not encode a legal promise in a comment beside a delete call.
The catch is operational ownership. A self-managed component may offer control over policy evaluation and migration, but it also creates patching, monitoring, restore, and incident work. A managed service may reduce that burden while introducing provider-specific policy syntax and migration constraints. Stick with the managed option when the team cannot staff restore drills and policy reconciliation; choose a more controllable design when portability or a specialized retention rule is worth the on-call load.
Here is the core deletion path in Go. It deliberately asks the application database for the deadline and makes the delete decision only after the record is still eligible. The storage client is an interface so the policy can be tested without contacting a provider.
package retention
import (
"context"
"time"
)
type Document struct {
Key string
DeletionDueAt time.Time
LegalHold bool
Authorization string
}
type Documents interface {
Find(ctx context.Context, key string) (Document, error)
}
type Objects interface {
Delete(ctx context.Context, key string) error
}
func DeleteIfDue(ctx context.Context, docs Documents, objects Objects, key string, now time.Time) error {
doc, err := docs.Find(ctx, key)
if err != nil {
return err
}
if doc.LegalHold || now.Before(doc.DeletionDueAt) {
return nil
}
return objects.Delete(ctx, doc.Key)
}
The important behavior is not the interface name. It is the guard. A queue retry must be harmless before the deadline, and a legal hold must win over an automated cleanup event. In real code, the delete operation also needs an audit event, a retry policy, and a reconciliation report for objects that no longer have a matching database record.
A small SLO makes the comparison honest
Write the promise in observable terms: authorized tenants can retrieve an available document; unauthorized tenants cannot; eligible objects enter the deletion workflow by their deadline; held objects remain available to authorized readers; and a restore drill can recover the required backup copy. The exact target belongs to the business and compliance owners, not to the storage vendor's feature list.
Measure policy evaluation lag, authorization denials, orphan count, deletion failures, restore success, and retained bytes by tenant and classification. Alert on the trend as well as the threshold. A bucket that is growing because cleanup is silently skipping a new key prefix is a capacity problem and a data-governance problem at the same time.
This approach is not suitable when the workload needs public static delivery, unrestricted anonymous downloads, or retention semantics the chosen storage contract cannot prove. Pick a service and architecture that provide those controls instead of weakening the document policy to fit a convenient API. For ordinary private documents with a clear deadline, lifecycle cleanup plus application-owned authorization and reconciliation is a defensible boundary.
The conclusion is deliberately modest: compare S3, R2, and Wasabi lifecycle behavior as implementations of a tested contract, not as interchangeable labels. Keep custody, access, deletion, and recovery as separate responsibilities, then choose the option whose evidence fits the SLO and whose operational burden the team can actually carry.
Top comments (0)