The important choice for property-management training files is not which object store has the most attractive free allowance. It is whether access control or delivery simplicity owns the design. I would put access control first: a small, private document repository with explicit retention states is easier to audit and migrate than a fast public download path that later has to be made safe. The free tier is useful for a prototype, but it must not become the retention policy.

TL;DR: keep uploaded training artifacts private, derive a short-lived delivery authorization from an application decision, and encode retention as data rather than as a promise made by a dashboard setting. A three-gate lifecycle works well here: quarantine, active record, and expired. Compare storage services only after measuring the limits that affect those gates: private access semantics, lifecycle controls, deletion evidence, egress behavior, and migration options.

What does a free allowance actually protect?

A free allowance protects a budget boundary. It does not automatically protect a lease agreement, an employee record, or a training certificate. Those are different concerns, and combining them creates an uncomfortable failure mode: an artifact remains downloadable because the storage object still exists, even though the application has decided that the person should no longer see it.

For a property manager, the artifact might be a PDF safety course, a unit-inspection video, or a signed acknowledgement. The system should store an object key, an owner or property scope, a classification, an expiry timestamp, and a policy version beside the object. The object key is an address; it is not authorization.

That boundary matters.

Keep it private.

That distinction also changes how a provider comparison should be read. Cloudflare R2 exposes an S3-compatible object API and documents a monthly free allocation; Backblaze B2 documents a free storage allowance and an S3-compatible API; MinIO is a self-hosted object-storage system rather than a hosted free tier. These are materially different operating models. The useful comparison is not a leaderboard. It is which boundary your team can operate and verify.

I would record the allowance and its measurement period in configuration, then test behavior against a small synthetic dataset. Provider limits and terms change. A design that depends on a remembered number is already stale.

The three gates that make retention reproducible

The first gate is quarantine. New uploads have an untrusted status, a generated key, and no user-facing download permission. Validate the file type using content inspection as well as the supplied filename, cap size, reject unexpected extensions, and keep the object outside any public serving path. OWASP's file-upload guidance also calls out filename safety, authorization, storage location, and malware analysis; treating those as one control is a common mistake.

The second gate is active record. A worker, or an explicitly reviewed workflow, changes the artifact to active only after the checks that apply to the organization have completed. The application can then issue a short-lived signed request or stream the object through an authorization endpoint. The choice depends on delivery volume and latency, but neither mechanism should let a caller invent an object key and bypass the policy lookup.

The third gate is expired. Expiration should be an observable state transition before physical deletion. That gives the system a place to record who changed the policy, which policy version was applied, and whether a legal hold or investigation blocked deletion. A storage lifecycle rule can remove bytes later, but it cannot replace the application record of why deletion was allowed.

Here is the decision record I would keep with each artifact. It is deliberately boring. Boring data is easier to replay six months later.

from dataclasses import dataclass from datetime import datetime from enum import Enum class State(str, Enum): QUARANTINE = "quarantine" ACTIVE = "active" EXPIRED = "expired" @dataclass(frozen=True) class TrainingArtifact: object_key: str property_id: str classification: str state: State expires_at: datetime policy_version: int legal_hold: bool = False def can_deliver(artifact: TrainingArtifact, now: datetime) -> bool: return ( artifact.state is State.ACTIVE and not artifact.legal_hold and now < artifact.expires_at ) 

The function is not a security system by itself. It makes the rule testable, which is the point. Tests should cover a quarantined file, an expired file, a held file, a property-scope mismatch, and a request made after the delivery token's deadline.

Where do access control and delivery simplicity disagree?

A direct object response is operationally simple: the application authenticates, obtains a temporary authorization, and the client downloads from storage. The trade-off is that the authorization lifetime, cache behavior, and object visibility must all be understood. An application proxy gives tighter control over every byte, but it adds bandwidth, connection handling, and observability work.

Design choice Access-control strength Delivery cost Failure to test
Short-lived direct download Good when object visibility stays private Lower application bandwidth Expired or replayed authorization
Application proxy Central policy and response filtering Higher service load Memory, timeout, and range-request behavior
Public object URL Weak for sensitive training records Simple client path Link forwarding and accidental indexing

The last row is attractive during a demo and difficult to defend later. A URL is a capability: anyone who obtains it may be able to use it until it expires or is revoked. For training records that include names, signatures, or property details, that is a policy decision, not a convenience setting.

A practical test matrix should include a user who belongs to property A requesting a property B object, a revoked employee reusing a previously issued URL, a client retrying after expiry, and two simultaneous deletion jobs. The expected result is explicit in each case. Logging only a 403 response is not enough; log the policy decision, artifact identifier, and policy version without placing sensitive content or bearer tokens in the log.

Which alternative storage providers fit a free tier for storing user files?

I would compare three layers separately. First, check the interface: S3 compatibility can reduce application changes, but compatibility is not identity semantics, lifecycle semantics, or audit semantics. Second, check the control plane: private-by-default behavior, lifecycle filters, object versioning, retention or legal-hold support, event delivery, and export paths. Third, check the operating boundary: who patches the service, who monitors failed deletion, and where egress enters the budget.

Cloudflare R2's documented egress model can make delivery economics easier to reason about for some architectures, while its application still has to enforce tenant and property authorization. Backblaze B2 provides an object-storage service with an S3-compatible interface, but the same question remains: can the team prove that a policy transition happened and that a revoked link no longer works? MinIO gives a team more control over placement and operation because it can run on infrastructure the team manages; that control also makes patching, disks, replication, and recovery the team's responsibility.

Those differences are more relevant than a free-storage headline. A free allocation can disappear from the plan without changing the security model; a missing deletion event or an ambiguous ownership boundary can invalidate the model. I would keep the storage adapter narrow so a migration changes configuration and tests rather than every controller.

A rollout that leaves evidence behind

Start with 100 synthetic artifacts across three properties, including oversized files, duplicate names, expired records, and a legal hold. Use a fake clock in policy tests, then run a smaller end-to-end test against the chosen service. Confirm that the object is private, that the application rejects a scope mismatch, that a delivery authorization expires, and that the expiration worker is idempotent.

Deploy the quarantine path before importing historical files. During migration, write the policy version and source identifier into the metadata database, copy objects with checksums, and keep the source available until a sampled read-back and deletion report agree. Do not call a copy complete merely because a batch job reached 100 percent; verify the records that users actually need.

The resulting rule is compact: use the free allowance to validate volume and delivery behavior, never to define retention. Put authorization in the application policy, put lifecycle cleanup in a separately monitored job, and preserve enough state to explain every transition. Delivery can then be optimized without quietly becoming the owner of access decisions.

Sources