swiftops-backend / docs /api /user-profile /USER_DOCUMENT_MANAGEMENT.md
kamau1's picture
Make account_name optional and auto-populate it for mobile money and bank accounts
a37b051

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

  1. Document Types
  2. Profile Photo Management
  3. Identity Documents
  4. General Document Endpoints
  5. Frontend Integration
  6. 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, professional
  • description (optional) - Description of the document
  • tags (optional) - JSON array of tags
  • is_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: cloudinary or supabase

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

  1. Profile Photo:

    • Use clear, professional photo
    • Square aspect ratio works best
    • File size under 5MB
  2. Identity Documents:

    • Upload clear, readable scans
    • Include both front and back (if applicable)
    • Use descriptive names
    • Keep documents up to date

For Developers

  1. Always validate file types before upload
  2. Show upload progress for better UX
  3. Handle errors gracefully (network issues, file too large)
  4. Refresh signed URLs before they expire (1 hour)
  5. Cache profile photos in frontend state
  6. 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