Skip to main content

Flexible Object Storage: Supporting MinIO, Local, and R2 for Complete Deployment Freedom

· 6 min read
Sam Baker
OpenLifter

One of the biggest decisions when building fitness software is where to store user files - progress photos, exercise videos, profile pictures, and other assets. Most apps lock you into their preferred cloud provider or don't support file uploads at all. We built something different: a flexible object storage system that works whether you're running on a laptop, self-hosting with MinIO, or scaling with Cloudflare R2.

The File Storage Dilemma

Here's the problem with most fitness apps and file storage: they either don't support it, or they force you into their specific cloud setup. Want to self-host? Too bad, the app only works with AWS S3. Prefer local storage for privacy? Not an option. Need to switch providers later? Time for a complete rewrite.

This approach creates artificial limitations. Your choice of where to store files shouldn't dictate how you can deploy your fitness platform. Whether you're running a personal instance on your home server or scaling to thousands of users, the storage layer should adapt to your needs, not the other way around.

We wanted to build something that gives you complete control over where your data lives while maintaining the same feature set regardless of your storage choice.

Our Approach: Provider-Agnostic Object Storage

We built a flexible object storage abstraction that adapts to your deployment needs while providing the same features regardless of which backend you choose.

1. Clean Provider Interface

All storage providers implement the same interface, ensuring consistent functionality:

interface IStorageProvider {
uploadObject(input: ProviderUploadInput): Promise<ProviderUploadOutput>;
getObjectStream(input: ProviderGetInput): Promise<ProviderGetOutput>;
deleteObject(input: ProviderDeleteInput): Promise<ProviderDeleteOutput>;
generatePresignedGetUrl(input: ProviderPresignedInput): Promise<ProviderPresignedOutput>;
generatePresignedUploadUrl(input: ProviderPresignedUploadInput): Promise<ProviderPresignedOutput>;
checkObjectExists(input: ProviderExistsInput): Promise<boolean>;
}

Whether you're using local filesystem, MinIO, or Cloudflare R2, the application code remains identical. Switch providers with just a configuration change.

2. Local Storage Provider

Perfect for development, personal instances, and privacy-focused deployments:

# Local storage configuration
OBJECT_STORAGE_PROVIDER=LOCAL
LOCAL_STORAGE_BASE_PATH=./data/object-storage
LOCAL_STORAGE_BASE_URL=https://your-domain.com/api/storage
LOCAL_STORAGE_SECRET=your-signing-secret-32-chars-minimum
LOCAL_STORAGE_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,webp,heic,heif,mp4,mov,avi
LOCAL_STORAGE_MAX_FILE_SIZE=10485760 # 10MB

The local provider creates organized directory structures and implements its own signed URL system using HMAC-SHA256 tokens - no external dependencies required.

3. MinIO Provider

Self-hosted S3-compatible storage with full control:

# MinIO configuration
OBJECT_STORAGE_PROVIDER=MINIO
MINIO_ENDPOINT=https://minio.your-domain.com
MINIO_PORT=9000
MINIO_ACCESS_KEY=your-minio-access-key
MINIO_SECRET_KEY=your-minio-secret-key
MINIO_USE_SSL=true
MINIO_USER_ASSETS_BUCKET=user-assets
MINIO_APP_ASSETS_BUCKET=app-assets
MINIO_REGION=us-east-1

MinIO gives you S3 compatibility while keeping everything on your infrastructure. Perfect for organizations that need cloud-like scalability without cloud dependencies.

4. Cloudflare R2 Provider

Scalable cloud storage without egress fees:

# Cloudflare R2 configuration
OBJECT_STORAGE_PROVIDER=R2
R2_ACCOUNT_ID=your-cloudflare-account-id
R2_ACCESS_KEY_ID=your-r2-access-key
R2_SECRET_ACCESS_KEY=your-r2-secret-key
R2_USER_ASSETS_BUCKET=openlift-user-assets
R2_APP_ASSETS_BUCKET=openlift-app-assets
R2_USER_ASSETS_DOMAIN=assets.your-domain.com
R2_APP_ASSETS_DOMAIN=static.your-domain.com

R2 provides the scalability and reliability of cloud storage while eliminating the typical egress costs that make other providers expensive at scale.

How Provider Selection Works

The system automatically detects your provider configuration and initializes the appropriate backend:

// Factory pattern selects provider based on environment
const provider = ObjectStorageProviderFactory.create(config);

// All providers support the same operations
const uploadResult = await provider.uploadObject({
bucketType: StorageBucketType.USER_ASSETS,
key: 'users/12345/profile-picture/photo.jpg',
stream: fileStream,
contentType: 'image/jpeg',
size: fileSize
});

Provider-Specific Features

Each provider implements features using their optimal approach:

Local Storage:

  • Directory-based organization (./data/object-storage/user-assets/users/...)
  • Custom signed URLs with HMAC tokens
  • Direct filesystem operations for maximum performance

MinIO:

  • S3-compatible bucket operations
  • Native presigned URL generation
  • Built-in redundancy and replication support

Cloudflare R2:

  • Global CDN distribution
  • Custom domain support for branded URLs
  • Zero egress fees regardless of bandwidth usage

Security Across Providers

All providers implement consistent security measures:

  • File validation with MIME type and extension checking
  • Size limits configurable per asset type and deployment
  • Access control through time-limited signed URLs
  • Path sanitization to prevent traversal attacks

The security model remains identical whether files are stored locally or in the cloud.

Real-World Deployment Scenarios

Development & Testing

Start with local storage for development - zero configuration and instant feedback:

# Quick local setup
mkdir -p ./data/object-storage
export OBJECT_STORAGE_PROVIDER=LOCAL
export LOCAL_STORAGE_BASE_PATH=./data/object-storage
export LOCAL_STORAGE_SECRET=$(openssl rand -hex 32)
npm run dev

Personal Self-Hosting

Local storage works perfectly for personal instances. Your data stays completely private and you control retention policies:

# docker-compose.yml
services:
openlift:
environment:
- OBJECT_STORAGE_PROVIDER=LOCAL
- LOCAL_STORAGE_BASE_PATH=/app/data/storage
volumes:
- ./storage-data:/app/data/storage

Team & Organization Deployments

MinIO provides S3 compatibility with complete control over your infrastructure:

# docker-compose.yml with MinIO
services:
minio:
image: minio/minio
command: server /data --console-address ":9001"
volumes:
- minio-data:/data
environment:
- MINIO_ROOT_USER=your-access-key
- MINIO_ROOT_PASSWORD=your-secret-key

openlift:
environment:
- OBJECT_STORAGE_PROVIDER=MINIO
- MINIO_ENDPOINT=http://minio:9000
- MINIO_ACCESS_KEY=your-access-key
- MINIO_SECRET_KEY=your-secret-key

Scaling to Production

Cloudflare R2 handles the complexity of global distribution and eliminates egress fees:

# Production R2 configuration
OBJECT_STORAGE_PROVIDER=R2
R2_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-r2-key
R2_SECRET_ACCESS_KEY=your-r2-secret
R2_USER_ASSETS_BUCKET=prod-user-assets
R2_USER_ASSETS_DOMAIN=assets.yourapp.com

What This Storage Foundation Enables

Object storage infrastructure unlocks visual features across OpenLift:

  • Profile Photos: User avatars and identity (available now)
  • Progress Photo Galleries: Before/after comparisons with timeline views
  • Exercise Demonstration Videos: Custom form cues and technique notes
  • Workout Documentation: Equipment setup and form reference photos
  • Coach Content Sharing: Instructional materials and technique feedback
  • Program Assets: Exercise diagrams and supplemental educational content

The challenge was building storage infrastructure that works reliably across all deployment scenarios. With that foundation in place, adding visual features becomes straightforward.

Choose Your Storage, Keep Your Features

Object storage in OpenLift adapts to your deployment needs without compromising functionality. Whether you're running on a Raspberry Pi with local storage or scaling globally with R2, you get the same feature set and the same user experience.

Start with local storage, migrate to MinIO as you grow, or jump straight to R2 for global scale. Your storage choice is a deployment decision, not a feature limitation.

Check out the object storage documentation for complete setup instructions for each provider.


OpenLift is open-source fitness infrastructure that puts you in control of your training data. Star us on GitHub and join our community building the future of fitness software.