R2 Manual Versioning Implementation Plan
Context
Cloudflare R2 does not support native object versioning (unlike AWS S3). This is by design to keep R2 simple and cost-effective. However, for institutional requirements around file recovery and audit trails, we need a versioning strategy for user uploads (avatars, maintenance attachments).
Proposed Solution: Application-Level Versioning
Strategy Overview
Instead of relying on storage-level versioning, implement versioning at the application level using:
- Timestamp-based naming for automatic versioning
- Trash bucket approach for manual cleanup
- Database tracking for version management
Implementation Approach
1. File Naming Convention
Current: avatars/{userId}/{filename}New: avatars/{userId}/{timestamp}_{filename}
Example:
avatars/123/1640995200_profile.jpg (original upload)
avatars/123/1641081600_profile.jpg (updated version)
avatars/123/1641168000_profile.jpg (latest version)2. Database Schema Enhancement
Add versioning tracking to avatar and attachment tables:
-- Add to avatars table
ALTER TABLE avatars ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE avatars ADD COLUMN previous_versions TEXT[]; -- array of old keys
-- Add to maintenance_attachments table
ALTER TABLE maintenance_attachments ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE maintenance_attachments ADD COLUMN previous_versions TEXT[];3. Storage Service Enhancement
Update StorageService class:
class StorageService {
// Generate versioned key
static avatarKey(userId: string, filename: string): string {
const timestamp = Date.now();
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
return `avatars/${userId}/${timestamp}_${safeName}`;
}
// Move to trash instead of delete
async softDelete(key: string): Promise<void> {
const trashKey = `trash/${Date.now()}_${key}`;
await this.copy(key, trashKey);
await this.delete(key);
}
}4. Trash Management Strategy
Option A: Dedicated Trash Bucket
- Create separate R2 bucket:
cepatedge-trash - Move deleted files there with retention period
- Admin can review/approve permanent deletion
Option B: Prefixed Trash in Same Bucket
- Keep in same bucket with
trash/prefix - Lifecycle rules can auto-delete after 30/90 days
- Manual admin review process
5. Admin Interface
Add admin endpoints for trash management:
GET /admin/trash- List trash itemsPOST /admin/trash/:id/restore- Restore fileDELETE /admin/trash/:id- Permanent deletion
Timeline & Effort
Phase: Medium-priority task for Phase 4.5 completion or Phase 5 Effort: 4-6 hours implementation + testing Impact: High (addresses institutional file recovery requirements)
Benefits
- Institutional Compliance: Provides file recovery capabilities universities expect
- Cost Effective: No storage cost premium (unlike native versioning)
- Flexible: Can implement retention policies suitable for each institution
- Transparent: Clear audit trail of file changes
Migration Strategy
For existing files:
- Run migration script to add version=1 to existing records
- Keep existing files as-is (backward compatibility)
- New uploads use versioned naming
Success Criteria
- ✅ Files can be "deleted" but recovered from trash
- ✅ Version history visible to admins
- ✅ Automatic cleanup after configurable retention period
- ✅ No breaking changes to existing API
- ✅ Clear audit trail for compliance
Alternative: Accept Current State
For pilot phase, document that:
- Files are not versioned (R2 limitation)
- Recovery requires database restore + manual recreation
- Acceptable risk for initial pilot with small data volumes
Recommendation
Implement Option B (prefixed trash) as it's simpler and meets institutional requirements without additional infrastructure complexity.
This provides the file recovery capabilities universities expect while working within R2's design constraints.