6/8 - Public Beta (Discord)
|See Changelog
Skip to main content

Profile Photos API

The Profile Photos API provides secure file upload and management for user profile images. Built on OpenLift's enterprise-grade object storage infrastructure, it supports both local storage and cloud providers with automatic retention management.

Overview

Profile photos are stored using OpenLift's ObjectStorageService with the following features:

  • Multiple Storage Backends: Local filesystem or Cloudflare R2
  • Automatic Retention: Keeps 5 most recent photos, archives older ones
  • Signed URLs: Time-limited secure access to images
  • MIME Type Validation: Only approved image formats accepted
  • File Size Limits: 5MB maximum for profile photos

Upload Profile Photo

Upload a new profile photo for the authenticated user.

Endpoint

POST /api/storage/profile-photo/upload

Authentication

Requires valid JWT token in Authorization header.

Request Format

This endpoint uses multipart/form-data for file uploads.

Parameters

ParameterTypeRequiredDescription
fileFileYesImage file (JPEG, PNG, WebP, GIF, HEIC, HEIF)
assetContextStringNoDefaults to PROFILE_PICTURE

Supported File Types

  • JPEG (.jpg, .jpeg)
  • PNG (.png)
  • WebP (.webp)
  • GIF (.gif)
  • HEIC (.heic) - iOS photos
  • HEIF (.heif) - iOS photos

File Size Limits

  • Profile Photos: 5MB maximum
  • File validation includes both MIME type and file extension checking

Response

{
"success": true,
"data": {
"fileKey": "users/64abc123def456789/profile-picture/f47ac10b-58cc-4372-a567-0e02b2c3d479.jpg",
"uploadUrl": "https://api.openlift.app/api/storage/signed/user-assets/users/64abc123def456789/profile-picture/f47ac10b-58cc-4372-a567-0e02b2c3d479.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"fileName": "profile-photo.jpg",
"contentType": "image/jpeg",
"size": 245760,
"assetId": "64def789abc123456",
"uploadedAt": "2025-09-23T10:30:00.000Z",
"expiresAt": "2025-09-23T11:30:00.000Z"
}
}

Example Usage

curl -X POST \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "file=@profile-photo.jpg" \
"https://api.openlift.app/api/storage/profile-photo/upload"

Get Profile Photo URL

Retrieve the current profile photo URL for a user via GraphQL.

GraphQL Query

query GetUserProfile($userId: ID!) {
user(id: $userId) {
id
username
profilePhotoUrl
profilePhotoUploadedAt
}
}

Variables

{
"userId": "64abc123def456789"
}

Response

{
"data": {
"user": {
"id": "64abc123def456789",
"username": "johndoe",
"profilePhotoUrl": "https://api.openlift.app/api/storage/signed/user-assets/users/64abc123def456789/profile-picture/f47ac10b-58cc-4372-a567-0e02b2c3d479.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"profilePhotoUploadedAt": "2025-09-23T10:30:00.000Z"
}
}
}

Notes

  • profilePhotoUrl returns a signed URL valid for 1 hour
  • Returns null if no profile photo has been uploaded
  • profilePhotoUploadedAt indicates when the current photo was uploaded

Signed URL Access

Profile photos are served through signed URLs that provide secure, time-limited access without requiring authentication.

URL Format

https://api.openlift.app/api/storage/signed/{bucket}/{filePath}?token={signedToken}

URL Components

  • bucket: Storage bucket (user-assets for profile photos)
  • filePath: Complete file path including user ID and filename
  • signedToken: HMAC-signed token containing access permissions

Token Security

  • Expiration: URLs expire after 1 hour by default
  • Operation Validation: Tokens are bound to specific operations (GET only)
  • Path Validation: Tokens validate against the exact file path
  • HMAC Signature: Prevents token tampering using server secret

Error Responses

{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Token has expired"
}
}

Automatic Retention Management

The system automatically manages profile photo retention:

Retention Policy

  • Active Photos: 5 most recent uploads remain immediately accessible
  • Archived Photos: Older photos are marked as archived but not deleted
  • Automatic Cleanup: Happens immediately after each new upload

Retention Process

  1. New photo uploaded successfully
  2. System counts active profile photos for user
  3. If count exceeds 5, oldest photos are marked as archived: true
  4. Archived photos remain in database and storage but don't appear in API responses

Data Preservation

  • No Data Loss: Photos are archived, never deleted
  • Full Audit Trail: Complete upload history maintained in database
  • Recovery Possible: Archived photos can be restored if needed

Client Integration Guide

⚠️ CRITICAL: THIN CLIENT IMPLEMENTATION ONLY

What Clients SHOULD do:

  • ✅ Handle file selection and upload UI
  • ✅ Display profile photos from signed URLs
  • ✅ Manage upload progress and error states
  • ✅ Cache signed URLs according to expiration

What Clients MUST NOT do:

  • ❌ Implement file storage logic
  • ❌ Generate signed URLs locally
  • ❌ Handle retention policies
  • ❌ Validate file types beyond basic UI feedback

Implementation Checklist

File Upload Flow

  1. File Selection: Allow users to select image files
  2. Client Validation: Basic file type and size checking for UX
  3. Upload Request: POST to /api/storage/profile-photo/upload
  4. Progress Tracking: Show upload progress to user
  5. Error Handling: Display validation errors clearly
  6. Success Handling: Update UI with new profile photo URL

Display Flow

  1. Query Profile: Fetch user profile via GraphQL
  2. Check URL: Verify profilePhotoUrl is not null
  3. Display Image: Load image from signed URL
  4. Handle Expiration: Refresh URL when it expires (1 hour)
  5. Fallback: Show default avatar if no photo exists

Error Handling

  • Network Errors: Retry with exponential backoff
  • Validation Errors: Show specific error messages to user
  • Expired URLs: Refetch user profile to get new signed URL
  • File Too Large: Guide user to resize image

Flutter Integration Example

class ProfilePhotoService {
// Upload new profile photo
Future<ProfilePhotoUpload> uploadProfilePhoto(File imageFile) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$baseUrl/api/storage/profile-photo/upload'),
);

request.headers['Authorization'] = 'Bearer $token';
request.files.add(
await http.MultipartFile.fromPath('file', imageFile.path)
);

final response = await request.send();
final responseBody = await response.stream.bytesToString();

if (response.statusCode == 201) {
return ProfilePhotoUpload.fromJson(jsonDecode(responseBody));
} else {
throw ApiException.fromResponse(response.statusCode, responseBody);
}
}

// Get current profile photo URL
Future<String?> getProfilePhotoUrl(String userId) async {
const query = '''
query GetUserProfile(\$userId: ID!) {
user(id: \$userId) {
profilePhotoUrl
}
}
''';

final result = await graphqlClient.query(
QueryOptions(
document: gql(query),
variables: {'userId': userId},
),
);

return result.data?['user']?['profilePhotoUrl'];
}
}

Storage Configuration

Local Storage Provider

For self-hosted instances using local storage:

# 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-min
LOCAL_STORAGE_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,webp,heic,heif
LOCAL_STORAGE_MAX_FILE_SIZE=5242880 # 5MB

Cloudflare R2 Provider

For cloud storage with R2:

# R2 configuration
OBJECT_STORAGE_PROVIDER=R2
R2_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-access-key
R2_SECRET_ACCESS_KEY=your-secret-key
R2_USER_ASSETS_BUCKET=openlift-user-assets
R2_APP_ASSETS_BUCKET=openlift-app-assets

Rate Limiting

Profile photo uploads are subject to standard API rate limits:

  • Burst Limit: 100 requests per minute
  • Sustained Rate: 1000 requests per hour
  • File Size Impact: Larger files consume more rate limit budget

Security Considerations

File Validation

  • Double Validation: Both MIME type and file extension checked
  • Magic Number Verification: Planned for future release
  • Content Scanning: Can be integrated with external services

Access Control

  • Upload Authentication: Requires valid JWT token
  • User Isolation: Users can only upload to their own profile
  • URL Security: Signed URLs prevent unauthorized access

Privacy

  • Data Ownership: Users own their uploaded photos
  • Export Support: All photos included in data export
  • Deletion Rights: Users can request photo deletion (GDPR compliance)

Troubleshooting

Common Upload Issues

File Too Large

  • Profile photos limited to 5MB
  • Compress images before upload
  • Check file size in client before attempting upload

Unsupported File Type

  • Only image formats accepted
  • Convert videos/documents to supported image types
  • Check file extension matches content type

Network Timeouts

  • Large files may timeout on slow connections
  • Implement retry logic with exponential backoff
  • Consider image compression for faster uploads

Signed URL Issues

URL Expired

  • Signed URLs expire after 1 hour
  • Refetch user profile to get new URL
  • Implement automatic refresh logic

Token Invalid

  • Don't modify signed URLs
  • URLs are tied to specific files and operations
  • Generate new URLs rather than reusing old ones

For questions about the Profile Photos API, check our GitHub Discussions or open an issue.