App upload storage is the architecture choice that determines whether user files stay tied to one application server or live in a separate durable storage layer.
Once customers depend on uploaded files, the decision becomes part of production reliability rather than simple file handling. Raff Technologies supports 3,000+ customers and 15,000+ VMs, and one pattern matters repeatedly for small teams: application compute should be replaceable without putting customer files at risk.
This guide focuses on that narrow decision. Use S3-Compatible Object Storage Use Cases for Developers for the wider object-storage model, and Object Storage vs Block Storage vs VM Disk when you are comparing all three storage layers.
App uploads become production data once users depend on them
App uploads are files a product receives, generates, stores, serves, or processes on behalf of users. Common examples include profile images, PDFs, invoices, CSV imports, product media, attachments, reports, exports, audio, video, and design assets.
These files behave differently from app code and database records.
App code should be reproducible from source or build artifacts. Database records are structured application state. Uploads are usually binary objects with their own growth, retention, access, recovery, and migration requirements.
A small application often begins with a local path:
App VM ↓ /var/www/app/uploads
That is a reasonable starting point when files are temporary or low-risk. The architecture changes when the same directory becomes the source of truth for customer data. At that point, a server rebuild is no longer only a compute event; it can become a file-recovery event.
At Raff, the practical design principle is simple: durable customer files should not depend on infrastructure the team expects to rebuild, resize, replace, or scale independently.
The decision framework separates temporary files from durable files
The right storage choice depends on file value, lifetime, access pattern, growth, and recovery needs.

| Situation | Better default | Why |
|---|---|---|
| Prototype or demo | VM disk | Lowest operational overhead |
| Temporary processing file | VM disk | Local scratch space is appropriate |
| Regenerable thumbnail or cache | VM disk or object storage | Choose from processing and delivery needs |
| Internal tool with a few low-risk files | VM disk can work | Simplicity may outweigh separation |
| Customer uploads are part of the product | Object storage | Files should survive app-server changes |
| Upload growth is unpredictable | Object storage | Capacity grows separately from compute |
| Two or more app servers need the same files | Object storage | Shared object access avoids per-server copies |
| Private downloads need temporary access | Object storage | Presigned URL workflows fit controlled delivery |
| Reports, exports, media, or attachments must be retained | Object storage | These are durable file objects |
| App VM is being resized mainly for disk capacity | Object storage | Compute and file capacity should scale separately |
| File relationships and ownership matter | Database metadata + object storage | Structured state and file bodies stay separate |
A useful production rule is:
Store ownership, status, and object keys in the database. Store durable file bodies in object storage.
This rule is not absolute. Tiny files with strong transactional requirements can belong in a database, and a single VM can safely host modest low-risk files. The point is to separate data when separation reduces a real operational risk.
VM disk is acceptable while the server can remain the file owner
VM disk is still the simplest choice for prototypes, temporary files, staging workflows, build artifacts that can be recreated, and small internal applications.
It works well when:
- the application has one server;
- files are small and predictable;
- uploads can be regenerated or lost without serious impact;
- backup and restore are already tested;
- the VM has enough storage headroom;
- no other service needs shared access to the files;
- file retention is short and simple.
The advantage is direct filesystem access. The application can read and write normal paths without an object-storage SDK or API.
The disadvantage is coupling. Uploads share the server lifecycle with application code, logs, temporary files, package caches, and possibly database files. A full disk can affect the whole product. A second app server creates a file-synchronization problem. A server replacement can require a separate file migration.
VM disk becomes the wrong default when the team starts buying a larger server only because uploads are growing. That is a storage-capacity problem, not necessarily a CPU or RAM problem.
Define the exit condition before local uploads become difficult to move. Good triggers include customer dependency on file retention, unpredictable storage growth, multi-server deployment, slow VM backups, or a requirement that uploads survive a full server rebuild.
Object storage is the safer production boundary for durable uploads
Object storage separates durable file data from application compute. The app stores each file as an object in a bucket and keeps the corresponding business metadata in a database.
A production pattern looks like this:
User uploads file ↓ Application validates request ↓ Object storage receives file ↓ Database stores owner, object key, type, size, and status
This boundary changes several operational properties at once.
Compute becomes easier to replace. The app server can be rebuilt, resized, or duplicated without copying the entire upload directory first.
Storage can grow for storage reasons. File growth no longer forces the team to buy additional CPU or RAM.
Multiple application instances can share the same objects. A second web server or worker does not need a synchronized local upload tree.
Controlled access becomes easier to model. AWS documents presigned URLs as a method for granting time-limited object access without making the object public: AWS S3 presigned URL documentation. Raff supports the same S3-style presigned URL workflow.
Object storage is therefore a strong fit for:
- user images and attachments;
- PDFs and documents;
- generated reports and exports;
- product media;
- large downloads;
- files processed by workers or functions;
- retained backup artifacts;
- static assets that should outlive one VM.
It is not a replacement for every filesystem use case. Temporary working files, software that requires normal mounted-disk semantics, and active database data belong on disk or block storage rather than in an object bucket.
Metadata, security, and access controls belong in the application design
Moving uploads to object storage does not remove application responsibility. It gives the team a clearer place to enforce that responsibility.
A typical upload record should keep structured state such as:
| Field | Purpose |
|---|---|
id | Internal file identifier |
account_id or user_id | Ownership boundary |
object_key | Bucket object path |
original_filename | User-facing name |
content_type | MIME type |
size_bytes | File size |
visibility | Private, public, or controlled |
checksum | Integrity or deduplication signal |
status | Uploaded, processing, ready, failed, deleted |
created_at | Upload timestamp |
deleted_at | Deletion workflow state |
The database answers business questions: who owns the file, which record it belongs to, whether the user can access it, and whether processing succeeded. Object storage answers file-storage questions: which object exists, how large it is, and how it is retrieved.
Upload security should also cover file validation, size limits, malware scanning where appropriate, authorization, private-by-default access, credential scope, deletion rules, and logging. The OWASP File Upload Cheat Sheet highlights risks including malicious content, unrestricted retrieval, parser vulnerabilities, and storage exhaustion.
A safer access flow is:
User requests upload or download ↓ App checks account, permission, and file rules ↓ App issues controlled S3 operation or presigned URL ↓ Object storage handles file transfer
Avoid treating an unguessable object key as authorization. The application should still verify tenant and user permissions before granting access.
Cost and product limits should be part of the storage decision
Current Raff Object Storage pricing is designed around one predictable base plan:
| Pricing item | Current Raff Object Storage |
|---|---|
| Base price | $7/month |
| Included storage | 100 GB |
| Included egress | 1 TB/month |
| API requests | Unlimited, no request charge |
| Additional storage | $0.07/GB-month |
| Additional egress | $0.01/GB |
| Inbound transfer | $0 |
The S3-compatible endpoint is s3.raffusercloud.com. Traffic from Raff VMs to buckets in the same region is not charged as public egress.
This cost shape is useful for upload-heavy SaaS products because storage capacity can grow without resizing application compute. It also makes transfer cost explicit once the included 1 TB of monthly egress is exceeded.
Product limits matter just as much as price. Raff currently supports objects up to 5 TiB through multipart upload. The platform supports presigned URLs, bucket policies, ACLs, and scoped access keys. Versioning, lifecycle rules, and object lock are not currently supported, so applications that need automatic retention transitions, immutable object retention, or built-in version history must design those requirements separately.
That limitation affects deletion and retention design. If the product must retain customer files for 30, 90, or 365 days, the application or an external workflow should enforce that policy rather than assuming bucket lifecycle automation exists.
The economic decision should still include engineering time. Object storage introduces SDK/API integration and a separate data layer, but it can remove the recurring operational cost of synchronizing files across servers, expanding VM disks for uploads, and moving customer files during compute changes.
Migration should make object storage the source of truth
The safest migration from VM disk to object storage is staged around one clear end state: after the migration, durable uploads no longer depend on the old local directory.
A practical migration sequence is:
- Clean up the file metadata model. Store file ownership, current local path, target object key, type, size, and status in a structured record.
- Create the bucket and access model. Decide which application component can upload, read, and delete objects.
- Move existing files. Copy the current upload set to object storage and verify object counts, sizes, checksums where practical, and representative downloads.
- Switch new writes. New durable uploads should go to object storage rather than local disk.
- Support a temporary read fallback if needed. During migration, the app can read from object storage first and fall back to the old path until the old set is complete.
- Verify production behavior. Test upload, download, delete, permissions, file processing, exports, and backup/recovery assumptions.
- Remove the local dependency. Delete old local files only after the object copy and application flows are proven.
Monitor upload failures, object-storage usage, permission errors, database-to-object consistency, failed processing jobs, and download errors during the transition.
The biggest migration mistake is dual ownership with no end date. If some durable files remain authoritative on VM disk while others are authoritative in object storage, recovery and debugging become harder. Pick one source of truth and complete the move.
Raff maps uploads, metadata, and compute to separate layers
Raff supports the common production separation directly.

A typical stack is:
Users ↓ Raff VM or Raff Apps = application runtime ↓ Raff Managed Database or database VM = metadata and ownership ↓ Raff Object Storage = durable uploads and generated files
Use VM disk for the operating system, app runtime, package cache, temporary processing workspace, and short-lived local files. Use object storage for customer uploads, media, reports, exports, attachments, large application files, and retained backup objects.
This division keeps the responsibilities clear. Compute can change for performance or deployment reasons. Database capacity can change for structured-data reasons. File storage can change because customers are uploading more content.
The first-hand architecture principle is the same one used earlier in this guide: if a server must remain alive because it contains customer uploads, the file layer is controlling the compute lifecycle. Moving durable uploads into object storage removes that dependency and gives the team more freedom to rebuild or scale the application layer.
App uploads belong outside replaceable compute when they matter
VM disk is a good tool for prototypes, temporary processing, caches, and small low-risk workloads. It stops being the cleanest default when customer files become durable product data, when multiple application instances need the same files, or when storage growth starts driving compute decisions.
Object storage gives durable uploads their own capacity, access, cost, and recovery boundary. Keep file metadata and authorization in the database, keep the file body in object storage, and keep the application server replaceable.
Continue with S3-Compatible Object Storage Use Cases for Developers for broader S3 patterns or Object Storage vs Block Storage vs VM Disk for the wider storage decision.
