7/9 - Released
|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.