Spaces:
Sleeping
User Document Management API
Overview
The User Document Management system allows users to upload and manage their personal documents including profile photos, identity cards, licenses, certificates, and other compliance documents.
Key Features:
- Profile photo upload with automatic optimization (Cloudinary)
- Identity document storage (ID cards, licenses, certificates)
- Document versioning and history
- Secure storage with Supabase and Cloudinary
- Automatic signed URL generation for secure access
- Document expiry tracking
Table of Contents
- Document Types
- Profile Photo Management
- Identity Documents
- General Document Endpoints
- Frontend Integration
- Security & Permissions
Document Types
Common User Document Types
| Type | Description | Storage | Public |
|---|---|---|---|
profile_photo |
User profile picture | Cloudinary | Yes |
national_id |
National ID card | Supabase | No |
passport |
Passport | Supabase | No |
driver_license |
Driver's license | Supabase | No |
certificate |
Professional certificates | Supabase | No |
contract |
Employment contract | Supabase | No |
resume |
CV/Resume | Supabase | No |
medical_certificate |
Medical fitness certificate | Supabase | No |
police_clearance |
Police clearance certificate | Supabase | No |
Profile Photo Management
Upload Profile Photo
Endpoint: POST /api/v1/documents/users/{user_id}/profile-photo
Description: Upload or update user profile photo. Automatically creates new version and optimizes image via Cloudinary.
Request:
POST /api/v1/documents/users/me/profile-photo
Content-Type: multipart/form-data
Authorization: Bearer <token>
file: <image_file>
Response:
{
"id": "doc_123",
"entity_type": "user",
"entity_id": "user_456",
"file_name": "profile.jpg",
"file_type": "image/jpeg",
"file_size": 245678,
"file_url": "https://res.cloudinary.com/...",
"storage_provider": "cloudinary",
"document_type": "profile_photo",
"document_category": "profile",
"version": 2,
"is_latest_version": true,
"description": "User profile photo",
"tags": ["profile", "photo"],
"is_public": true,
"uploader": {
"id": "user_456",
"name": "John Doe",
"email": "john@example.com"
},
"created_at": "2025-12-02T10:30:00Z",
"updated_at": "2025-12-02T10:30:00Z"
}
Features:
- β Automatic image optimization (resize, compress)
- β CDN delivery for fast loading
- β Version history maintained
- β Old photos marked as outdated (not deleted)
- β Public access for display in UI
Get Current Profile Photo
Endpoint: GET /api/v1/documents/users/{user_id}/profile-photo
Description: Get user's current profile photo (latest version).
Response:
{
"id": "doc_123",
"file_url": "https://res.cloudinary.com/...",
"file_name": "profile.jpg",
"version": 2,
"is_latest_version": true,
"created_at": "2025-12-02T10:30:00Z"
}
Returns: null if no profile photo exists
Get Profile Photo History
Endpoint: GET /api/v1/documents/users/{user_id}/profile-photo/versions
Description: Get all versions of user's profile photo (history).
Response:
[
{
"id": "doc_123",
"version": 2,
"file_url": "https://res.cloudinary.com/.../v2.jpg",
"is_latest_version": true,
"created_at": "2025-12-02T10:30:00Z"
},
{
"id": "doc_122",
"version": 1,
"file_url": "https://res.cloudinary.com/.../v1.jpg",
"is_latest_version": false,
"created_at": "2025-11-15T08:20:00Z"
}
]
Identity Documents
Upload Identity Document
Endpoint: POST /api/v1/documents/users/{user_id}/upload
Description: Upload identity documents (ID card, passport, license, certificates).
Request:
POST /api/v1/documents/users/me/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
file: <document_file>
document_type: national_id
document_category: identity
description: National ID Card - Front
tags: ["identity", "verification"]
is_public: false
Parameters:
file(required) - Document file (image or PDF)document_type(required) - Type of document (see table above)document_category(optional) - Category: identity, legal, compliance, professionaldescription(optional) - Description of the documenttags(optional) - JSON array of tagsis_public(optional) - Whether document is publicly accessible (default: false)
Response:
{
"id": "doc_789",
"entity_type": "user",
"entity_id": "user_456",
"file_name": "national_id_front.jpg",
"file_type": "image/jpeg",
"file_size": 1245678,
"file_url": "supabase://documents/users/user_456/national_id_front.jpg",
"storage_provider": "supabase",
"document_type": "national_id",
"document_category": "identity",
"version": 1,
"is_latest_version": true,
"description": "National ID Card - Front",
"tags": ["identity", "verification"],
"is_public": false,
"created_at": "2025-12-02T10:35:00Z"
}
Note: Identity documents are stored in Supabase Storage (not Cloudinary) for security and compliance.
Get User Documents
Endpoint: GET /api/v1/documents/users/{user_id}
Description: Get all documents for a user, optionally filtered by type.
Query Parameters:
document_type(optional) - Filter by document type
Examples:
# Get all documents
GET /api/v1/documents/users/me
# Get only identity documents
GET /api/v1/documents/users/me?document_type=national_id
# Get all certificates
GET /api/v1/documents/users/me?document_type=certificate
Response:
{
"total": 5,
"documents": [
{
"id": "doc_789",
"document_type": "national_id",
"file_name": "national_id_front.jpg",
"file_url": "https://supabase.co/storage/v1/object/sign/...",
"created_at": "2025-12-02T10:35:00Z"
},
{
"id": "doc_790",
"document_type": "driver_license",
"file_name": "license.pdf",
"file_url": "https://supabase.co/storage/v1/object/sign/...",
"created_at": "2025-11-20T14:20:00Z"
}
]
}
Note: Supabase URLs are automatically signed with 1-hour expiry for security.
Get Specific Document
Endpoint: GET /api/v1/documents/id/{document_id}
Description: Get a specific document by ID with fresh signed URL.
Response:
{
"id": "doc_789",
"file_url": "https://supabase.co/storage/v1/object/sign/...",
"file_name": "national_id_front.jpg",
"file_size": 1245678,
"document_type": "national_id",
"created_at": "2025-12-02T10:35:00Z",
"uploader": {
"id": "user_456",
"name": "John Doe",
"email": "john@example.com"
}
}
Delete Document
Endpoint: DELETE /api/v1/documents/id/{document_id}
Description: Delete a document (soft delete in DB, actual file deletion from storage).
Response: 204 No Content
General Document Endpoints
Universal Upload
Endpoint: POST /api/v1/documents/upload
Description: Universal document upload for any entity type.
Request:
POST /api/v1/documents/upload
Content-Type: multipart/form-data
file: <file>
entity_type: user
entity_id: user_456
document_type: certificate
document_category: professional
description: AWS Certification
tags: ["aws", "cloud", "certification"]
is_public: false
force_provider: supabase # Optional: force storage provider
Parameters:
force_provider(optional) - Override storage provider:cloudinaryorsupabase
Get Entity Documents
Endpoint: GET /api/v1/documents/{entity_type}/{entity_id}
Description: Get all documents for any entity.
Examples:
GET /api/v1/documents/user/user_456
GET /api/v1/documents/ticket/ticket_789
GET /api/v1/documents/project/project_123
Document Versioning
Get All Versions:
GET /api/v1/documents/{entity_type}/{entity_id}/{document_type}/versions
Get Latest Version:
GET /api/v1/documents/{entity_type}/{entity_id}/{document_type}/latest
Frontend Integration
Profile Photo Upload Flow
// Upload profile photo
async function uploadProfilePhoto(file: File) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/v1/documents/users/me/profile-photo', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const data = await response.json();
// Update UI with new photo URL
setProfilePhotoUrl(data.file_url);
return data;
}
Identity Document Upload Flow
// Upload identity document
async function uploadIdentityDocument(
file: File,
documentType: string,
description: string
) {
const formData = new FormData();
formData.append('file', file);
formData.append('document_type', documentType);
formData.append('document_category', 'identity');
formData.append('description', description);
formData.append('tags', JSON.stringify(['identity', 'verification']));
formData.append('is_public', 'false');
const response = await fetch('/api/v1/documents/users/me/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
return await response.json();
}
Fetch User Documents
// Get all user documents
async function getUserDocuments(documentType?: string) {
const url = documentType
? `/api/v1/documents/users/me?document_type=${documentType}`
: '/api/v1/documents/users/me';
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
return data.documents;
}
Display Profile Photo
// React component
function UserAvatar({ userId }: { userId: string }) {
const [photoUrl, setPhotoUrl] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/v1/documents/users/${userId}/profile-photo`, {
headers: { 'Authorization': `Bearer ${token}` }
})
.then(res => res.json())
.then(data => {
if (data) {
setPhotoUrl(data.file_url);
}
});
}, [userId]);
return (
<img
src={photoUrl || '/default-avatar.png'}
alt="Profile"
className="w-12 h-12 rounded-full"
/>
);
}
Document Upload Component
function DocumentUploadForm() {
const [file, setFile] = useState<File | null>(null);
const [documentType, setDocumentType] = useState('national_id');
const [uploading, setUploading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) return;
setUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('document_type', documentType);
formData.append('document_category', 'identity');
formData.append('is_public', 'false');
const response = await fetch('/api/v1/documents/users/me/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (response.ok) {
toast.success('Document uploaded successfully');
// Refresh document list
} else {
toast.error('Upload failed');
}
} finally {
setUploading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<select
value={documentType}
onChange={(e) => setDocumentType(e.target.value)}
>
<option value="national_id">National ID</option>
<option value="passport">Passport</option>
<option value="driver_license">Driver's License</option>
<option value="certificate">Certificate</option>
</select>
<input
type="file"
accept="image/*,application/pdf"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
<button type="submit" disabled={!file || uploading}>
{uploading ? 'Uploading...' : 'Upload Document'}
</button>
</form>
);
}
Security & Permissions
Access Control
Profile Photos:
- β Public access (anyone can view)
- β Only owner or admin can upload/update
- β Stored in Cloudinary CDN
Identity Documents:
- β Private (not public)
- β Only owner or admin can view
- β Signed URLs with 1-hour expiry
- β Stored in Supabase Storage
Permission Requirements
| Action | Permission | Notes |
|---|---|---|
| Upload document | upload_documents |
Auto-granted to all users for own documents |
| View own documents | view_documents |
Auto-granted to all users |
| View other's documents | view_documents + admin role |
Admins/managers only |
| Delete document | upload_documents |
Can delete own documents |
Row-Level Security
Users can only:
- Upload documents to their own profile
- View their own documents
- Delete their own documents
Admins/managers can:
- View documents of users they manage
- Upload documents on behalf of users
- Cannot delete documents (audit trail)
File Validation
Profile Photos:
- Must be image file (JPEG, PNG, GIF, WebP)
- Max size: 10MB
- Automatically resized and optimized
Identity Documents:
- Accepts images and PDFs
- Max size: 20MB
- No automatic processing (preserve original)
Storage Providers
Cloudinary (Profile Photos)
Advantages:
- Automatic image optimization
- CDN delivery worldwide
- Image transformations on-the-fly
- Fast loading
Use Cases:
- Profile photos
- Public images
- Images needing optimization
Supabase Storage (Identity Documents)
Advantages:
- Secure private storage
- Signed URLs with expiry
- Compliance-friendly
- Cost-effective
Use Cases:
- Identity documents
- Private files
- Compliance documents
- Large files
Document Expiry Tracking
Set Document Expiry
When uploading documents with expiry dates (licenses, certificates):
// Upload with expiry tracking
const formData = new FormData();
formData.append('file', file);
formData.append('document_type', 'driver_license');
formData.append('issued_at', '2023-01-15'); // Optional
formData.append('expires_at', '2028-01-15'); // Optional
Check Expired Documents
-- Backend query for expired documents
SELECT * FROM user_document_links
WHERE expires_at < CURRENT_DATE
AND expires_at IS NOT NULL;
Best Practices
For Users
Profile Photo:
- Use clear, professional photo
- Square aspect ratio works best
- File size under 5MB
Identity Documents:
- Upload clear, readable scans
- Include both front and back (if applicable)
- Use descriptive names
- Keep documents up to date
For Developers
- Always validate file types before upload
- Show upload progress for better UX
- Handle errors gracefully (network issues, file too large)
- Refresh signed URLs before they expire (1 hour)
- Cache profile photos in frontend state
- Implement retry logic for failed uploads
Error Handling
Common Errors
400 Bad Request - Invalid File Type:
{
"detail": "Profile photo must be an image"
}
400 Bad Request - File Too Large:
{
"detail": "File size exceeds maximum allowed (10MB)"
}
404 Not Found - Document Not Found:
{
"detail": "Document not found"
}
500 Internal Server Error - Upload Failed:
{
"detail": "Upload failed: Connection timeout"
}
Complete Example: Profile Setup Flow
// Complete profile setup with photo and documents
async function completeProfileSetup(
profilePhoto: File,
nationalId: File,
driverLicense: File
) {
try {
// 1. Upload profile photo
const photoData = await uploadProfilePhoto(profilePhoto);
console.log('Profile photo uploaded:', photoData.file_url);
// 2. Upload national ID
const idData = await uploadIdentityDocument(
nationalId,
'national_id',
'National ID Card'
);
console.log('National ID uploaded');
// 3. Upload driver's license
const licenseData = await uploadIdentityDocument(
driverLicense,
'driver_license',
'Driver License'
);
console.log('Driver license uploaded');
// 4. Mark profile as complete
await fetch('/api/v1/profile/me/basic', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'active'
})
});
return {
success: true,
profilePhoto: photoData.file_url
};
} catch (error) {
console.error('Profile setup failed:', error);
throw error;
}
}
Last Updated: December 2, 2025
API Version: v1
Maintained By: SwiftOps Backend Team