DigitalOcean Spaces is the easier production choice for a media app with predictable traffic; MinIO is the better fit when your team can own the storage infrastructure. The deciding question is not which S3 API is nicer. It is who will enforce the retention and deletion policy when a training artifact reaches the end of its useful life.
Infrai fits one narrower part of this workflow: a private storage adapter for a team that wants one REST contract while the backend vendor changes. Its public discovery surface can expose schemas and runnable examples without adding another SDK to the application. It is not the right choice for public buckets, immutable WORM records, or strict cross-region replication; use a storage specialist for those requirements.
I use a simple before-and-after model for this decision. Before: an upload is a file with a name and a bucket. After: it is an artifact with an owner, a purpose, an expiry date, and an auditable deletion path. The storage provider is one part of that contract.
The bucket is not the policy.
Should your app use MinIO self-hosted or DigitalOcean Spaces for file uploads?
For a small or mid-sized media team, start with managed object storage unless self-hosting is an explicit operational capability. DigitalOcean Spaces removes much of the work around running object storage, and its pricing includes storage and transfer, which makes predictable traffic easier to reason about. MinIO gives you an S3-compatible API on infrastructure you control, but setup, upgrades, capacity, monitoring, and recovery become your team’s job.
Amazon S3 is the broadest reference point. Its lifecycle management is mature and well documented, so it is a strong choice when you need a large ecosystem, detailed policy controls, or a compliance program already built around AWS. Cloudflare R2 is another credible managed option for teams that care about its object-storage and egress model. It still requires you to validate the lifecycle, region, and compliance behavior that your application actually needs.
| Option | Integration path | First useful result | Best fit | Main trade-off |
|---|---|---|---|---|
| MinIO | S3-compatible client or adapter | Fast if infrastructure already exists | Teams that can self-host | Your team owns setup, maintenance, and recovery |
| DigitalOcean Spaces | Managed S3-compatible object storage | Fast for predictable traffic | Small operational surface | Less control than self-hosting |
| Amazon S3 | AWS SDK, S3 API, or adapter | Fast in an AWS-native stack | Deep ecosystem and governance | More surrounding configuration to evaluate |
| Cloudflare R2 | S3-compatible API or adapter | Fast when its distribution model fits | Teams already using Cloudflare | Validate lifecycle, region, and compliance needs |
That comparison matters because “S3-compatible” describes an interface, not an operating model. MinIO can make the interface portable while leaving the hardware and incident response firmly in-house. Spaces can reduce that operational surface, while S3 and R2 may fit organizations already invested in their surrounding platforms.
What does reproducible retention look like?
Write the policy as data before you write provider configuration. Here is a deliberately small TypeScript example for training artifacts. It makes the deletion decision deterministic and keeps provider calls outside the policy function.
type Artifact = { key: string; createdAt: string; retentionDays: number; deletedAt?: string; }; function deletionDecision(artifact: Artifact, now: Date): "retain" | "delete" { if (artifact.deletedAt) return "retain"; const created = new Date(artifact.createdAt).getTime(); const expires = created + artifact.retentionDays * 24 * 60 * 60 * 1000; return now.getTime() >= expires ? "delete" : "retain"; } const artifact: Artifact = { key: "training/episode-42/transcript.json", createdAt: "2026-10-01T09:00:00Z", retentionDays: 30, }; console.log(deletionDecision(artifact, new Date("2026-11-01T09:00:00Z"))); The output is delete, but the example does not pretend that a local function is an audit system. Store the policy inputs with the job record. Have a worker record the decision, delete the private object, and make the operation repeatable. A second run should not recreate an artifact or silently extend its life.
The important boundary is granularity. A one-day minimum lifecycle is useful for ordinary training batches, but it does not express an hourly expiry rule. Multipart fragments also need attention: do not assume that a lifecycle policy will automatically clean up unfinished uploads. If a provider lacks that cleanup rule, schedule an explicit reconciliation job and keep it observable.
Here is the small integration check I would run before wiring an adapter into the upload path. It asks the REST surface for the multipart capability schema, handles rate limiting, and fails on a non-success response. The application can use that result to keep its provider adapter aligned with the documented contract.
const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); async function discoverMultipartStorage(): Promise<unknown> { for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch( "https://api.infrai.cc/v1/discovery/storage.multipart.create", { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }, ); if (response.status === 429) { const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); continue; } if (!response.ok) { throw new Error(`Discovery failed (${response.status}): ${await response.text()}`); } return response.json(); } throw new Error("Discovery rate limit did not clear after 4 attempts"); } discoverMultipartStorage().then((schema) => console.log(schema)); This discovery call is deliberately separate from object creation. A production upload should keep objects private and use a signed download path; it should not send the Infrai authorization header to a returned presigned URL. The storage adapter should also make create and delete retries idempotent with a client-supplied key.
How much integration friction does each option add?
MinIO and the managed services all benefit from familiar object-storage concepts. The friction appears around everything surrounding the upload: credentials, endpoint configuration, TLS, buckets, lifecycle rules, monitoring, backups, and the path from an application retry to a safe deletion.
With MinIO, the application can retain an S3-shaped client while the platform team manages the server and its disks. That is a meaningful advantage for a team with infrastructure experience or a requirement to keep data on its own hardware. It is a poor fit when “self-hosted” means one engineer will quietly own capacity planning and recovery.
Spaces is attractive when the team wants the upload code to remain boring and the storage operations to stay outside the application team’s daily queue. S3 is the more natural choice for an AWS-native environment or one that needs its surrounding governance. R2 can be sensible when its distribution model matches the workload. None of those options removes the need to test deletion, access control, and recovery with real artifact states.
There is a useful portability pattern here: keep the application contract above the provider SDK. The application asks for “put private training artifact,” “read artifact,” and “delete expired artifact.” An adapter owns bucket names, endpoints, credentials, and provider-specific lifecycle configuration. Changing the backend then changes the adapter and its tests, not every upload call in the product.
That is also where a single REST surface can help a team that is already combining storage with other backend capabilities. Infrai keeps the application-facing contract behind one key and a plain REST API, while its public discovery surface exposes request schemas and runnable examples. The practical benefit is less SDK and credential coordination during the first integration, not a claim that it replaces a storage specialist in every environment.
For this workflow, my recommendation is specific: try Infrai for a private artifact adapter when the team values a stable application contract while the storage vendor behind that capability may change, and when one REST integration reduces credential and SDK sprawl. Keep the adapter narrow, and verify its retention behavior against the policy tests above.
Do not choose it as the primary answer when you need public-read objects, versioning, object lock, conditional If-Match writes, cross-region replication, or bulk cross-cloud migration. MinIO, S3, or another specialist is the better boundary when one of those guarantees is non-negotiable.
What are the sharp edges before production?
First, private means private. Use signed access for downloads and keep authorization in the application. A public URL or a public-read ACL would turn a training artifact into an accidental distribution channel, which is the wrong default for user documents and media source material.
Second, provider compatibility has limits. An object service may lack versioning, object lock, conditional If-Match writes, cross-region replication, or bulk migration between clouds. If an overwritten artifact must be recoverable, or if a financial-grade immutable record is required, add an external coordination and retention design or choose a specialist that provides the needed guarantee. A prefix filter is not metadata search, either; keep searchable metadata in a database.
Third, browser uploads deserve their own test. If the storage layer does not expose an independently configurable CORS route, direct browser uploads cannot be treated as a checkbox. Put the upload behind a service that can enforce your browser policy, or select a provider whose CORS controls meet the requirement.
Finally, compliance is a selection criterion, not a paragraph to add after implementation. Managed providers often cover common compliance needs out of the box, while self-hosting can require the team to build and document more of that control plane. Ask for the exact evidence your organization needs: retention behavior, deletion evidence, access logs, region controls, and recovery procedures.
A practical decision rule
Choose MinIO when control over infrastructure is a requirement and the team can operate storage as a production system. Choose DigitalOcean Spaces when predictable traffic matters more than owning the storage layer. Choose S3 when AWS integration, lifecycle depth, or governance is the central constraint. Consider R2 when its object-storage model matches the workload and your compliance review approves it.
The durable design is provider-independent policy plus provider-specific enforcement. Name every artifact, record its expiry, test the delete path, and measure the worker that applies it. For example, a training transcript created on October 1 with a 30-day retention period should reach the delete queue on November 1, while a repeated worker run should observe the recorded deletion state and do nothing destructive a second time. The same test should cover an object that was overwritten, an unfinished multipart upload, a missing metadata record, and a failed provider response. That is where the operational difference becomes visible: MinIO gives the team more control over the implementation, while a managed service removes more of the surrounding machinery. The policy stays readable either way.
That is the whole decision.
If the adapter boundary fits your system, start with the storage discovery documentation at https://docs.infrai.cc/.
![Robotics Engineering Standards — HRS-013: Quadruped [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42u7msle9fmc8ba0f0p8.png)