Skip to content

LoRA User Storage Support Investigation

Date: 2025-11-13 Status: 📋 Investigation Complete Report Type: Feature Analysis & Design Proposal Stakeholders: EmProps Studio Team, Machine Infrastructure Team Related Systems: ComfyUI Custom Nodes, flat_file Storage, Machine Cache Management


Executive Summary

This report investigates adding user-owned LoRA (Low-Rank Adaptation) storage to EmProps, allowing users to upload and store their own LoRAs in their flat_file storage, then dynamically load them on machines with intelligent caching.

Current State:

  • ✅ LoRA loading infrastructure exists (EmProps_Lora_Loader)
  • ✅ Cloud storage download capabilities (Azure Blob Storage)
  • ✅ Model cache management with LRU eviction
  • ✅ User file storage via flat_file table
  • ❌ No user-owned LoRA storage/management
  • ❌ No per-user LoRA quota/limits
  • ❌ No automatic machine cache eviction based on age

Proposed Solution:

  • Store user LoRAs in flat_file table with tags=['lora']
  • Upload to Azure Blob Storage under user-specific container paths
  • Reserve dedicated LoRA cache space on machines (configurable, e.g., 50GB)
  • Just-in-time download from user's flat_file storage via Azure
  • LRU eviction when cache fills up
  • Time-based eviction (remove LoRAs not used for 1 week)
  • Seamless integration with existing EmProps_Lora_Loader node

Table of Contents

  1. Current Infrastructure
  2. Requirements
  3. Proposed Architecture
  4. Implementation Plan
  5. Database Schema Changes
  6. Machine Cache Management
  7. API Endpoints
  8. ComfyUI Integration
  9. Success Metrics
  10. Future Considerations

Current Infrastructure

Existing LoRA Loader

File: packages/comfyui-custom-nodes/emprops_comfy_nodes/nodes/emprops_lora_loader.py

Capabilities:

  • Downloads LoRAs from Azure Blob Storage (also supports AWS S3, GCS)
  • Caches locally in ComfyUI's loras/ directory
  • Integrates with ComfyUI's built-in LoraLoader
  • Supports strength parameters for model and CLIP

Current Flow:

User provides lora_name + cloud provider

Check if exists locally

If not, download from Azure Blob Storage (emprops-share/models/loras/{lora_name})

Load via ComfyUI's LoraLoader

Limitation: Only loads from shared cloud storage, not user-specific storage

Existing Model Cache System

File: packages/comfyui-custom-nodes/emprops_comfy_nodes/db/model_cache.py

Capabilities:

  • SQLite database tracking all models (checkpoints, LoRAs, VAEs)
  • LRU tracking via last_used timestamp and use_count
  • Protected models (won't be evicted)
  • Ignored models (is_ignore flag for system models)
  • Disk space management with min_free_space_gb setting
  • get_least_recently_used_models() for cache eviction

Database Schema:

sql
CREATE TABLE models (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  path TEXT NOT NULL UNIQUE,
  model_type TEXT NOT NULL,
  filename TEXT NOT NULL,
  size_bytes INTEGER NOT NULL,
  last_used TEXT NOT NULL,
  use_count INTEGER DEFAULT 0,
  download_date TEXT NOT NULL,
  protected INTEGER DEFAULT 0,
  is_ignore INTEGER DEFAULT 0
);

CREATE TABLE settings (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

Key Features:

  • Automatic LRU eviction when disk space is low
  • Per-model usage tracking
  • Configurable minimum free space threshold

Existing User File Storage

File: packages/database-schema/prisma/schema.prisma

Model: flat_file

prisma
model flat_file {
  id                  BigInt                @id @default(autoincrement())
  created_at          DateTime?             @default(now())
  url                 String?
  hidden              Boolean?              @default(false)
  user_id             String                @db.Uuid
  name                String
  gen_in_data         Json?
  gen_out_data        Json?
  mime_type           String?
  rel_id              String?
  rel_type            String?
  tags                String[]              @default([])
  chat_message        chat_message[]
  component_flat_file component_flat_file[]
}

Usage: Stores user-uploaded images, videos, and other assets Storage Backend: Azure Blob Storage (via cloud storage savers)


Requirements

Functional Requirements

  1. User LoRA Upload

    • Users can upload .safetensors or .pt LoRA files
    • Files stored in user's flat_file storage with tags=['lora']
    • Upload via EmProps Studio UI
    • Validation: file format, size limits (e.g., max 2GB per LoRA)
  2. LoRA Discovery

    • Users can browse their uploaded LoRAs
    • Search by name, tags, upload date
    • Display file size, upload date, usage count
  3. LoRA Usage in Workflows

    • Workflows can reference user's LoRAs by flat_file.id
    • EmProps_Lora_Loader enhanced to support user LoRAs
    • Workflow JSON stores LoRA reference: {"lora_source": "user", "lora_id": "123"}
  4. Machine Cache Management

    • Reserved LoRA cache space on each machine (configurable, default: 50GB)
    • Just-in-time download when workflow references a LoRA
    • LRU eviction when cache is full
    • Time-based eviction: Remove LoRAs not used for 7 days
    • Separate eviction policy for user LoRAs vs. shared LoRAs
  5. Quota Management

    • Per-user LoRA storage quota (e.g., 10GB per user)
    • Admin configurable quotas
    • Warning when approaching quota limit

Non-Functional Requirements

  1. Performance

    • First-time LoRA download: <30 seconds for typical LoRA (200-500MB)
    • Cached LoRA load: <2 seconds
    • Parallel downloads for multiple LoRAs in same workflow
  2. Reliability

    • Graceful fallback if user LoRA fails to download
    • Retry mechanism with exponential backoff
    • Clear error messages to user
  3. Security

    • User LoRAs only accessible to the user who uploaded them
    • No cross-user LoRA access (unless explicitly shared)
    • Virus/malware scanning for uploaded LoRAs (future)
  4. Observability

    • Track LoRA download times (OpenTelemetry spans)
    • Monitor cache hit rate
    • Alert on repeated download failures

Proposed Architecture

High-Level Data Flow

┌─────────────────────────────────────────────────────────┐
│                  EmProps Studio UI                      │
│                (User uploads LoRA)                      │
└──────────────────┬──────────────────────────────────────┘

                   │ POST /api/loras/upload

┌─────────────────────────────────────────────────────────┐
│              EmProps API Server                         │
│  - Validate file format (.safetensors)                 │
│  - Check user quota                                     │
│  - Upload to cloud storage                              │
│  - Create flat_file record                              │
└──────────────────┬──────────────────────────────────────┘

                   │ INSERT INTO flat_file

┌─────────────────────────────────────────────────────────┐
│          PostgreSQL (flat_file table)                   │
│  {                                                      │
│    user_id: "uuid",                                     │
│    name: "my_style_lora.safetensors",                   │
│    url: "s3://bucket/users/{user_id}/loras/{file_id}",  │
│    tags: ["lora"],                                      │
│    mime_type: "application/octet-stream",               │
│    size_bytes: 456123789                                │
│  }                                                      │
└──────────────────┬──────────────────────────────────────┘

                   │ Workflow uses LoRA
                   │ {"lora_source": "user", "lora_id": "123"}

┌─────────────────────────────────────────────────────────┐
│            ComfyUI Worker (Machine)                     │
│  - Parse workflow JSON                                  │
│  - Detect user LoRA reference                           │
│  - Enhanced EmProps_Lora_Loader                         │
└──────────────────┬──────────────────────────────────────┘

                   │ Check local cache

┌─────────────────────────────────────────────────────────┐
│         Machine LoRA Cache                              │
│  Location: /workspace/ComfyUI/models/loras/users/       │
│  Structure: users/{user_id}/{lora_filename}             │
│                                                         │
│  Cache Hit? → Use existing file                         │
│  Cache Miss? → Download from flat_file.url              │
└──────────────────┬──────────────────────────────────────┘

                   │ (On cache miss)

┌─────────────────────────────────────────────────────────┐
│         Download & Cache Management                     │
│  1. Check cache quota (default: 50GB)                   │
│  2. If full, evict LRU user LoRAs                       │
│  3. Download from S3 to local cache                     │
│  4. Register in model_cache.db                          │
│  5. Load LoRA via ComfyUI                               │
└─────────────────────────────────────────────────────────┘

Storage Structure

Azure Blob Storage:

container/users/{user_id}/loras/
├── {flat_file_id_1}.safetensors
├── {flat_file_id_2}.safetensors
└── {flat_file_id_3}.pt

Machine Local Cache:

/workspace/ComfyUI/models/loras/
├── shared/                    # Shared LoRAs (existing behavior)
│   └── popular_style.safetensors
└── users/                     # User-specific LoRAs (new)
    ├── {user_id_1}/
    │   ├── my_style.safetensors
    │   └── anime_lora.safetensors
    └── {user_id_2}/
        └── portrait_lora.safetensors

Database Storage (PostgreSQL):

sql
-- flat_file table (existing, no schema change needed)
SELECT * FROM flat_file WHERE user_id = 'uuid' AND 'lora' = ANY(tags);

-- Example record:
{
  id: 123,
  user_id: "550e8400-e29b-41d4-a716-446655440000",
  name: "my_anime_style.safetensors",
  url: "https://emprops.blob.core.windows.net/user-loras/users/550e8400.../loras/123.safetensors",
  tags: ["lora"],
  mime_type: "application/octet-stream",
  created_at: "2025-11-13T10:00:00Z",
  gen_in_data: null,
  gen_out_data: {
    "size_bytes": 456123789,
    "file_hash": "sha256:abc123...",
    "lora_metadata": {
      "base_model": "SDXL",
      "trigger_words": ["anime style", "vibrant colors"]
    }
  }
}

Model Cache Database (SQLite on machine):

sql
-- models table (existing)
INSERT INTO models VALUES (
  path: '/workspace/ComfyUI/models/loras/users/550e8400.../my_anime_style.safetensors',
  model_type: 'user_lora',
  filename: 'my_anime_style.safetensors',
  size_bytes: 456123789,
  last_used: '2025-11-13T12:00:00',
  use_count: 5,
  download_date: '2025-11-13T10:05:00',
  protected: 0,
  is_ignore: 0
);

Implementation Plan

Phase 1: User LoRA Upload (Week 1)

Objective: Allow users to upload and store LoRAs in flat_file

Tasks:

  1. ✅ Create API endpoint: POST /api/loras/upload

    • Accept .safetensors and .pt files
    • Validate file format using file signature (magic bytes)
    • Check user quota before accepting upload
    • Upload to cloud storage under users/{user_id}/loras/{file_id}
    • Create flat_file record with tags=['lora']
  2. ✅ Create API endpoint: GET /api/loras

    • List user's LoRAs from flat_file
    • Filter: WHERE user_id = ? AND 'lora' = ANY(tags)
    • Return: id, name, size_bytes, created_at, url
  3. ✅ Create API endpoint: DELETE /api/loras/:id

    • Soft delete from flat_file (set hidden = true)
    • Delete from cloud storage (async job)
    • Invalidate machine caches (via Redis pub/sub)
  4. ✅ Add quota management

    • Query total LoRA size per user: SUM(size_bytes) WHERE user_id = ? AND 'lora' = ANY(tags)
    • Enforce limit (e.g., 10GB default)
    • Admin API to adjust quotas

Deliverables:

  • API endpoints for LoRA CRUD operations
  • File validation and upload logic
  • Quota tracking and enforcement
  • Unit tests for API endpoints

Success Criteria:

  • Users can upload LoRAs via API
  • Files stored in cloud storage with correct paths
  • flat_file records created with proper tags
  • Quota limits enforced

Phase 2: Enhanced LoRA Loader (Week 1-2)

Objective: Extend EmProps_Lora_Loader to support user LoRAs

Tasks:

  1. ✅ Enhance EmProps_Lora_Loader node

    • Add lora_source input: ["shared", "user"]
    • For user source, accept flat_file_id instead of filename
    • Query flat_file table to get URL
    • Download from user's cloud storage path
    • Cache in loras/users/{user_id}/ directory
  2. ✅ Create UserLoraDownloader utility class

    • Fetch flat_file record by ID
    • Verify user has access (via job's user_id context)
    • Download from flat_file.url to local cache
    • Handle authentication tokens for cloud storage
    • Track download progress (for UI feedback)
  3. ✅ Update workflow JSON schema

    • Support LoRA references: {"lora_source": "user", "lora_id": 123}
    • Backward compatible with existing lora_name string references
  4. ✅ Error handling

    • Graceful fallback if LoRA download fails
    • Clear error messages: "LoRA not found", "Download failed", "Quota exceeded"
    • Retry logic with exponential backoff

Deliverables:

  • Enhanced EmProps_Lora_Loader.py
  • UserLoraDownloader class
  • Updated workflow JSON schema documentation
  • Integration tests with mock cloud storage

Success Criteria:

  • Workflows can reference user LoRAs by flat_file_id
  • LoRAs download on first use
  • Subsequent uses load from cache
  • Clear errors on failures

Phase 3: Machine Cache Management (Week 2)

Objective: Implement LoRA cache eviction policies

Tasks:

  1. ✅ Add LoRA cache quota setting

    • New model_cache.db setting: lora_cache_max_gb (default: 50)
    • Separate from general model cache
    • Configurable per machine via environment variable
  2. ✅ Implement LRU eviction for user LoRAs

    • Before downloading, check if cache exceeds quota
    • Query get_least_recently_used_models() filtered by model_type='user_lora'
    • Delete oldest LoRAs until space available
    • Update model_cache.db after deletions
  3. ✅ Implement time-based eviction

    • Background job (cron): Run daily at 3 AM
    • Query models WHERE model_type='user_lora' AND last_used < NOW() - INTERVAL '7 days'
    • Delete files and remove from cache DB
    • Log eviction events for monitoring
  4. ✅ Add cache warming (optional optimization)

    • Pre-download frequently used user LoRAs
    • Analyze job history to predict likely LoRAs
    • Download during machine idle time

Deliverables:

  • LoRA cache quota management
  • LRU eviction logic
  • Time-based eviction cron job
  • Cache statistics API endpoint

Success Criteria:

  • Cache stays within configured quota
  • LRU eviction frees space as needed
  • LoRAs unused for 7 days are removed
  • No manual cache management needed

Phase 4: EmProps Studio UI Integration (Week 2-3)

Objective: User-facing UI for LoRA management

Tasks:

  1. ✅ Create LoRA Library page in EmProps Studio

    • Grid view of user's uploaded LoRAs
    • Display: thumbnail (if available), name, size, upload date
    • Actions: Download, Delete, View Details
  2. ✅ LoRA upload component

    • Drag-and-drop file upload
    • Progress bar during upload
    • Validation feedback (file format, size, quota)
    • Success/error notifications
  3. ✅ Workflow editor integration

    • LoRA picker component in node editor
    • Search and filter user's LoRAs
    • Preview LoRA metadata (base model, trigger words)
    • One-click add to workflow
  4. ✅ Usage analytics

    • Show LoRA usage count
    • Display which workflows use each LoRA
    • Identify unused LoRAs (suggest deletion)

Deliverables:

  • LoRA Library UI page
  • Upload component with progress tracking
  • Workflow editor LoRA picker
  • Usage analytics dashboard

Success Criteria:

  • Users can upload LoRAs via UI
  • LoRAs browsable and searchable
  • Easy integration into workflows
  • Usage insights help users manage storage

Phase 5: Monitoring & Optimization (Week 3)

Objective: Production-ready observability and performance tuning

Tasks:

  1. ✅ Add OpenTelemetry instrumentation

    • Trace LoRA downloads (duration, size, source)
    • Track cache hit/miss rates
    • Monitor eviction events
    • Alert on high download failure rates
  2. ✅ Create Dash0 dashboards

    • LoRA cache hit rate by machine
    • Average download time per LoRA size
    • Eviction frequency and reasons
    • User quota utilization
  3. ✅ Performance optimization

    • Implement parallel downloads for multi-LoRA workflows
    • Use HTTP range requests for resume support
    • Compress LoRAs in transit (if not already compressed)
    • CDN integration for frequently used LoRAs
  4. ✅ Documentation

    • User guide: "How to Upload and Use Custom LoRAs"
    • Developer docs: Cache eviction policies
    • Troubleshooting: Common LoRA errors
    • API reference: LoRA endpoints

Deliverables:

  • OpenTelemetry spans and metrics
  • Dash0 monitoring dashboards
  • Performance benchmarks
  • User and developer documentation

Success Criteria:

  • Full observability of LoRA subsystem
  • Cache hit rate >80% for repeat workflows
  • Download failures <1%
  • Documentation complete and reviewed

Database Schema Changes

flat_file Table (No Schema Change)

Existing schema is sufficient. We'll use tags array to mark LoRAs:

sql
-- Query to find user's LoRAs
SELECT * FROM flat_file
WHERE user_id = 'uuid'
  AND 'lora' = ANY(tags)
ORDER BY created_at DESC;

-- Query total LoRA storage per user
SELECT user_id, SUM(
  COALESCE(
    (gen_out_data->>'size_bytes')::bigint,
    0
  )
) as total_lora_bytes
FROM flat_file
WHERE 'lora' = ANY(tags)
GROUP BY user_id;

New gen_out_data Structure for LoRAs:

json
{
  "size_bytes": 456123789,
  "file_hash": "sha256:abc123...",
  "lora_metadata": {
    "base_model": "SDXL",
    "rank": 32,
    "trigger_words": ["anime style", "vibrant colors"],
    "training_epochs": 10,
    "training_images": 50
  },
  "upload_source": "emprops_studio",
  "validation_status": "safe"
}

model_cache.db Schema Enhancement (SQLite on machines)

Add new model_type: user_lora to distinguish from shared LoRAs

sql
-- Existing table structure (no schema change needed)
CREATE TABLE models (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  path TEXT NOT NULL UNIQUE,
  model_type TEXT NOT NULL,  -- e.g., 'user_lora', 'checkpoint', 'vae'
  filename TEXT NOT NULL,
  size_bytes INTEGER NOT NULL,
  last_used TEXT NOT NULL,
  use_count INTEGER DEFAULT 0,
  download_date TEXT NOT NULL,
  protected INTEGER DEFAULT 0,
  is_ignore INTEGER DEFAULT 0
);

-- Query user LoRAs for eviction
SELECT * FROM models
WHERE model_type = 'user_lora'
  AND protected = 0
  AND is_ignore = 0
ORDER BY last_used ASC
LIMIT 10;

New Settings:

sql
-- Add to settings table
INSERT INTO settings (key, value) VALUES
  ('lora_cache_max_gb', '50'),
  ('lora_max_age_days', '7'),
  ('lora_eviction_enabled', 'true');

Optional: user_lora_quota Table (Future Enhancement)

If we need per-user quota customization:

sql
CREATE TABLE user_lora_quota (
  user_id UUID PRIMARY KEY,
  max_storage_gb DECIMAL(10, 2) DEFAULT 10.00,
  current_storage_gb DECIMAL(10, 2) DEFAULT 0.00,
  max_lora_count INT DEFAULT 100,
  current_lora_count INT DEFAULT 0,
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Track quota usage
CREATE OR REPLACE FUNCTION update_user_lora_quota()
RETURNS TRIGGER AS $$
BEGIN
  -- Update quota on INSERT/UPDATE/DELETE of flat_file records with 'lora' tag
  IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
    INSERT INTO user_lora_quota (user_id, current_storage_gb, current_lora_count)
    SELECT
      user_id,
      SUM((gen_out_data->>'size_bytes')::bigint) / (1024.0 * 1024 * 1024),
      COUNT(*)
    FROM flat_file
    WHERE user_id = NEW.user_id AND 'lora' = ANY(tags) AND hidden = false
    GROUP BY user_id
    ON CONFLICT (user_id) DO UPDATE
    SET
      current_storage_gb = EXCLUDED.current_storage_gb,
      current_lora_count = EXCLUDED.current_lora_count,
      updated_at = NOW();
  END IF;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER flat_file_lora_quota_trigger
AFTER INSERT OR UPDATE OR DELETE ON flat_file
FOR EACH ROW
WHEN ('lora' = ANY(NEW.tags) OR 'lora' = ANY(OLD.tags))
EXECUTE FUNCTION update_user_lora_quota();

Machine Cache Management

Cache Eviction Policies

1. LRU (Least Recently Used) - Space-Based

Triggered when: LoRA download would exceed lora_cache_max_gb

python
def evict_lora_cache_for_space(required_bytes):
    """
    Evict user LoRAs until we have enough space
    """
    # Get current cache size
    user_lora_size = get_total_size_by_type('user_lora')
    cache_max_bytes = get_setting('lora_cache_max_gb', 50) * 1024**3

    # Check if we need to evict
    if user_lora_size + required_bytes > cache_max_bytes:
        space_to_free = (user_lora_size + required_bytes) - cache_max_bytes

        # Get LRU user LoRAs
        lru_loras = model_cache_db.get_least_recently_used_models(limit=50)
        lru_loras = [l for l in lru_loras if l['model_type'] == 'user_lora']

        freed_bytes = 0
        for lora in lru_loras:
            if freed_bytes >= space_to_free:
                break

            # Delete file
            if os.path.exists(lora['path']):
                os.remove(lora['path'])
                freed_bytes += lora['size_bytes']
                model_cache_db.delete_model(lora['path'])
                log_debug(f"Evicted LoRA (LRU): {lora['filename']} ({lora['size_bytes']} bytes)")

        return freed_bytes >= space_to_free

2. Time-Based Eviction - Age-Based

Triggered: Daily cron job at 3 AM

python
def evict_old_loras():
    """
    Remove user LoRAs not used in 7 days
    """
    max_age_days = int(get_setting('lora_max_age_days', 7))
    cutoff_date = datetime.now() - timedelta(days=max_age_days)

    # Query old user LoRAs
    old_loras = model_cache_db.get_models_by_criteria(
        model_type='user_lora',
        last_used_before=cutoff_date.isoformat(),
        protected=False
    )

    evicted_count = 0
    freed_bytes = 0

    for lora in old_loras:
        if os.path.exists(lora['path']):
            os.remove(lora['path'])
            freed_bytes += lora['size_bytes']
            model_cache_db.delete_model(lora['path'])
            evicted_count += 1
            log_debug(f"Evicted LoRA (age): {lora['filename']} (last used: {lora['last_used']})")

    log_debug(f"Time-based eviction: Removed {evicted_count} LoRAs, freed {freed_bytes} bytes")
    return evicted_count, freed_bytes

3. Quota-Based Eviction - User-Specific

Triggered: When user uploads new LoRA

python
def enforce_user_quota(user_id):
    """
    Ensure user's total LoRA storage doesn't exceed quota
    """
    # Get user quota
    quota_result = db.query(
        "SELECT max_storage_gb, current_storage_gb FROM user_lora_quota WHERE user_id = ?",
        [user_id]
    )

    if not quota_result:
        return True  # No quota set, allow

    max_storage_gb = quota_result['max_storage_gb']
    current_storage_gb = quota_result['current_storage_gb']

    if current_storage_gb > max_storage_gb:
        # User exceeded quota - prevent new uploads
        raise QuotaExceededError(
            f"LoRA storage quota exceeded: {current_storage_gb}GB / {max_storage_gb}GB"
        )

    return True

Cache Directory Structure

/workspace/ComfyUI/models/loras/
├── shared/                          # Shared/system LoRAs
│   ├── popular_style.safetensors    # Protected: is_ignore=1
│   └── official_sdxl.safetensors    # Protected: is_ignore=1

└── users/                           # User-specific LoRAs
    ├── 550e8400-e29b-41d4-a716-446655440000/
    │   ├── my_anime_style.safetensors       # Evictable
    │   ├── portrait_lora.safetensors        # Evictable
    │   └── landscape_lora.safetensors       # Evictable

    └── 61c88efd-3b52-4c9e-a5d2-7f8a9b3c1d0e/
        └── custom_faces.safetensors         # Evictable

Cache Statistics:

python
{
  "total_lora_cache_gb": 45.2,
  "max_lora_cache_gb": 50.0,
  "utilization_percent": 90.4,
  "user_lora_count": 23,
  "shared_lora_count": 5,
  "cache_hit_rate_24h": 0.87,
  "evictions_last_24h": 3,
  "oldest_lora_age_days": 12
}

API Endpoints

1. Upload LoRA

typescript
POST /api/loras/upload
Content-Type: multipart/form-data
Authorization: Bearer {user_token}

Body:
- file: (binary) .safetensors or .pt file
- name: (string) User-friendly name
- tags: (string[]) Optional tags, e.g., ["style", "anime"]
- metadata: (JSON) Optional metadata
  {
    "base_model": "SDXL",
    "trigger_words": ["anime style"],
    "description": "My custom anime style LoRA"
  }

Response 201:
{
  "id": 123,
  "name": "my_anime_style.safetensors",
  "url": "https://emprops.blob.core.windows.net/user-loras/users/{user_id}/loras/123.safetensors",
  "size_bytes": 456123789,
  "created_at": "2025-11-13T10:00:00Z",
  "tags": ["lora", "style", "anime"],
  "user_id": "550e8400-e29b-41d4-a716-446655440000"
}

Response 413 (Quota Exceeded):
{
  "error": "QUOTA_EXCEEDED",
  "message": "LoRA storage quota exceeded: 9.8GB / 10GB",
  "current_usage_gb": 9.8,
  "max_quota_gb": 10.0,
  "available_gb": 0.2
}

Response 400 (Invalid File):
{
  "error": "INVALID_FILE_FORMAT",
  "message": "File must be .safetensors or .pt format",
  "detected_format": "application/zip"
}

2. List User LoRAs

typescript
GET /api/loras
Authorization: Bearer {user_token}

Query Parameters:
- search?: string (search by name)
- tags?: string[] (filter by tags)
- limit?: number (default: 50)
- offset?: number (default: 0)
- sort?: "created_at" | "name" | "size" (default: "created_at")
- order?: "asc" | "desc" (default: "desc")

Response 200:
{
  "loras": [
    {
      "id": 123,
      "name": "my_anime_style.safetensors",
      "url": "https://emprops.blob.core.windows.net/user-loras/users/{user_id}/loras/123.safetensors",
      "size_bytes": 456123789,
      "created_at": "2025-11-13T10:00:00Z",
      "tags": ["lora", "style", "anime"],
      "usage_count": 15,
      "last_used_at": "2025-11-13T12:00:00Z",
      "metadata": {
        "base_model": "SDXL",
        "trigger_words": ["anime style"]
      }
    }
  ],
  "total": 5,
  "limit": 50,
  "offset": 0
}

3. Get LoRA Details

typescript
GET /api/loras/:id
Authorization: Bearer {user_token}

Response 200:
{
  "id": 123,
  "name": "my_anime_style.safetensors",
  "url": "https://emprops.blob.core.windows.net/user-loras/users/{user_id}/loras/123.safetensors",
  "size_bytes": 456123789,
  "created_at": "2025-11-13T10:00:00Z",
  "tags": ["lora", "style", "anime"],
  "usage_count": 15,
  "last_used_at": "2025-11-13T12:00:00Z",
  "metadata": {
    "base_model": "SDXL",
    "rank": 32,
    "trigger_words": ["anime style", "vibrant colors"],
    "description": "My custom anime style LoRA"
  },
  "workflows_using": [
    {"id": "workflow-1", "name": "Anime Portrait Generator"},
    {"id": "workflow-2", "name": "Landscape with Style"}
  ]
}

Response 404:
{
  "error": "LORA_NOT_FOUND",
  "message": "LoRA with ID 123 not found or access denied"
}

4. Delete LoRA

typescript
DELETE /api/loras/:id
Authorization: Bearer {user_token}

Response 204: (No Content)

Response 404:
{
  "error": "LORA_NOT_FOUND",
  "message": "LoRA with ID 123 not found or access denied"
}

Side Effects:
- Sets flat_file.hidden = true (soft delete)
- Queues async job to delete from cloud storage
- Publishes Redis event to invalidate machine caches
- Machines will evict cached file on next cleanup cycle

5. Get User Quota

typescript
GET /api/loras/quota
Authorization: Bearer {user_token}

Response 200:
{
  "max_storage_gb": 10.0,
  "current_storage_gb": 4.5,
  "available_gb": 5.5,
  "utilization_percent": 45.0,
  "max_lora_count": 100,
  "current_lora_count": 5,
  "loras": [
    {
      "id": 123,
      "name": "my_anime_style.safetensors",
      "size_gb": 0.45
    },
    ...
  ]
}

6. Get Cache Statistics (Admin)

typescript
GET /api/admin/loras/cache-stats
Authorization: Bearer {admin_token}

Query Parameters:
- machine_id?: string (specific machine, or all if omitted)

Response 200:
{
  "machines": [
    {
      "machine_id": "machine-1",
      "total_lora_cache_gb": 45.2,
      "max_lora_cache_gb": 50.0,
      "utilization_percent": 90.4,
      "user_lora_count": 23,
      "shared_lora_count": 5,
      "cache_hit_rate_24h": 0.87,
      "evictions_last_24h": 3,
      "oldest_lora_age_days": 12
    }
  ]
}

ComfyUI Integration

Enhanced EmProps_Lora_Loader Node

Updated INPUT_TYPES:

python
@classmethod
def INPUT_TYPES(cls):
    return {
        "required": {
            "model": ("MODEL",),
            "clip": ("CLIP",),
            "lora_source": (["shared", "user"], {
                "default": "shared",
                "tooltip": "Load from shared storage or user's personal LoRAs"
            }),
            "lora_identifier": ("STRING", {
                "default": "",
                "multiline": False,
                "tooltip": "For shared: filename (e.g., 'style.safetensors'). For user: flat_file ID (e.g., '123')"
            }),
            "strength_model": ("FLOAT", {
                "default": 1.0,
                "min": -10.0,
                "max": 10.0,
                "step": 0.01
            }),
            "strength_clip": ("FLOAT", {
                "default": 1.0,
                "min": -10.0,
                "max": 10.0,
                "step": 0.01
            }),
        }
    }

Updated load_lora Method:

python
def load_lora(self, model, clip, lora_source, lora_identifier, strength_model, strength_clip):
    """
    Load LoRA from either shared storage or user's personal storage
    """
    try:
        if lora_source == "user":
            # Load from user's flat_file storage
            lora_path = self.download_user_lora(lora_identifier)
        else:
            # Load from shared cloud storage (existing behavior)
            lora_path = self.download_from_cloud(lora_identifier)

        if lora_path is None:
            print(f"[EmProps] Could not find or download LoRA: {lora_identifier}")
            return (model, clip)

        # Load via ComfyUI's LoraLoader
        model_lora, clip_lora = self.lora_loader.load_lora(
            model,
            clip,
            os.path.basename(lora_path),
            strength_model,
            strength_clip
        )

        # Update usage tracking
        model_cache_db.update_model_usage(lora_path)

        return (model_lora, clip_lora)

    except Exception as e:
        print(f"[EmProps] Error loading LoRA: {str(e)}")
        return (model, clip)

New download_user_lora Method:

python
def download_user_lora(self, flat_file_id):
    """
    Download user's LoRA from flat_file storage
    """
    try:
        # Get job context to determine user_id
        user_id = self.get_current_user_id()  # From job metadata

        # Query flat_file record
        flat_file = db.query(
            "SELECT * FROM flat_file WHERE id = ? AND user_id = ? AND 'lora' = ANY(tags)",
            [flat_file_id, user_id]
        )

        if not flat_file:
            print(f"[EmProps] LoRA not found or access denied: {flat_file_id}")
            return None

        # Determine local cache path
        lora_dir = folder_paths.get_folder_paths("loras")[0]
        user_cache_dir = os.path.join(lora_dir, "users", user_id)
        os.makedirs(user_cache_dir, exist_ok=True)

        local_path = os.path.join(user_cache_dir, flat_file['name'])

        # Check if already cached
        if os.path.exists(local_path):
            print(f"[EmProps] User LoRA cache hit: {flat_file['name']}")
            return local_path

        # Check cache quota and evict if needed
        required_bytes = flat_file['gen_out_data']['size_bytes']
        if not evict_lora_cache_for_space(required_bytes):
            print(f"[EmProps] Failed to free space for LoRA download")
            return None

        # Download from Azure Blob Storage
        print(f"[EmProps] Downloading user LoRA: {flat_file['name']}")
        print(f"  FROM: {flat_file['url']}")
        print(f"    TO: {local_path}")

        handler = AzureHandler(container_name=self.user_container)
        success, error = handler.download_file(
            blob_path=flat_file['url'].split('/')[-1],  # Extract blob name from URL
            local_path=local_path
        )

        if not success:
            print(f"[EmProps] Error downloading user LoRA: {error}")
            return None

        # Register in cache database
        model_cache_db.register_model(
            path=local_path,
            model_type='user_lora',
            size_bytes=required_bytes,
            is_ignore=False
        )

        print(f"[EmProps] Successfully downloaded user LoRA: {flat_file['name']}")
        return local_path

    except Exception as e:
        print(f"[EmProps] Error in download_user_lora: {str(e)}")
        return None

Workflow JSON Schema

User LoRA Reference:

json
{
  "nodes": [
    {
      "id": 14,
      "type": "EmProps_Lora_Loader",
      "inputs": {
        "model": {"link": 27},
        "clip": {"link": 28},
        "lora_source": "user",
        "lora_identifier": "123",  // flat_file.id
        "strength_model": 0.75,
        "strength_clip": 1.0
      }
    }
  ]
}

Shared LoRA Reference (Backward Compatible):

json
{
  "nodes": [
    {
      "id": 14,
      "type": "EmProps_Lora_Loader",
      "inputs": {
        "model": {"link": 27},
        "clip": {"link": 28},
        "lora_source": "shared",
        "lora_identifier": "my_style.safetensors",
        "strength_model": 0.75,
        "strength_clip": 1.0
      }
    }
  ]
}

Success Metrics

System Health Metrics

LoRA Cache Performance:

  • Cache hit rate >80% for repeat workflows
  • Average download time <30s for typical LoRAs (200-500MB)
  • Eviction success rate >99% (space freed when needed)
  • Cache utilization 70-90% (not empty, not constantly full)

Upload & Storage:

  • Upload success rate >99.5%
  • Quota enforcement: 100% (no user exceeds quota)
  • File validation: 100% (only valid LoRA formats accepted)

Reliability:

  • LoRA load failures <1% (excluding user errors)
  • Download retry success >95% (transient failures recovered)
  • Data consistency: 100% (flat_file ↔ cloud storage sync)

User Experience Metrics

Workflow Performance:

  • 📊 First-time LoRA use - Time from job start to LoRA loaded
    • Target: <60 seconds (including download)
  • 📊 Cached LoRA use - Time to load from cache
    • Target: <5 seconds
  • 📊 Multi-LoRA workflows - Parallel download performance
    • Target: 3 LoRAs download in <90 seconds total

Upload Experience:

  • 📊 Upload time - Time to upload and confirm
    • Target: <2 minutes for 500MB LoRA
  • 📊 Upload failure rate - Percentage of failed uploads
    • Target: <0.5%

Storage Management:

  • 📊 Quota warnings - Users notified before hitting limit
    • Target: Warning at 80% quota usage
  • 📊 Eviction transparency - Users aware of machine evictions
    • Target: No user-visible impact (re-download is automatic)

Business Metrics

Adoption:

  • 📊 Users uploading LoRAs - Percentage of active users
    • Target: >20% of users upload at least 1 LoRA
  • 📊 LoRAs per user - Average LoRAs uploaded per user
    • Baseline: Measure after 1 month
  • 📊 Workflow usage - Percentage of workflows using user LoRAs
    • Target: >15% of new workflows

Storage Costs:

  • 📊 Average storage per user - Mean LoRA storage used
    • Monitor: Track monthly to predict costs
  • 📊 Storage growth rate - GB/month increase
    • Monitor: Adjust quotas if exceeds budget

Machine Efficiency:

  • 📊 Cache eviction frequency - Average evictions per machine per day
    • Target: <5 evictions/day (indicates good cache sizing)
  • 📊 Download bandwidth - GB downloaded per machine per day
    • Monitor: Optimize cache if bandwidth too high

Future Considerations

Phase 2 Enhancements (Months 2-3)

1. LoRA Sharing & Marketplace

  • Public LoRA library (users can publish their LoRAs)
  • Community ratings and reviews
  • Trending LoRAs dashboard
  • One-click import from marketplace

2. LoRA Collections

  • Group related LoRAs into collections
  • "Style packs" with multiple LoRAs
  • Share entire collections with other users
  • Collection presets for workflows

3. Advanced Metadata

  • Auto-extract LoRA metadata from safetensors file
  • Display training dataset info, base model compatibility
  • Trigger word suggestions
  • Compatibility warnings (e.g., "This LoRA may not work with SD 1.5")

4. Smart Cache Warming

  • Predict likely LoRAs based on user's workflow history
  • Pre-download during machine idle time
  • ML model to predict "hot" LoRAs per user
  • Reduce first-use latency to near-zero

Phase 3 Advanced Features (Months 4-6)

1. LoRA Version Control

  • Upload new versions of same LoRA
  • Track version history
  • Roll back to previous versions
  • Compare outputs between versions

2. LoRA Analytics

  • Heatmap of LoRA usage across user base
  • Identify "viral" LoRAs
  • Recommend LoRAs based on workflow similarity
  • A/B testing: Compare workflows with/without specific LoRAs

3. LoRA Training Integration

  • In-platform LoRA training (using DreamBooth/LoRA training)
  • Upload training images → get trained LoRA
  • Training progress tracking
  • Auto-upload trained LoRA to user's library

4. Advanced Eviction Strategies

  • Machine learning-based eviction (predict future use)
  • Per-user eviction policies (VIP users get longer cache)
  • Cross-machine LoRA migration (move to underutilized machines)
  • Federated cache (share LoRAs across machine pool)

Phase 4 Scale Optimizations (Months 6+)

1. CDN Integration

  • Serve frequently used LoRAs from CDN
  • Edge caching for low-latency downloads
  • Automatic cache invalidation on LoRA updates

2. P2P Caching

  • Machines share LoRAs with each other
  • Reduce cloud download bandwidth
  • Faster local network transfers

3. Compression & Deduplication

  • Compress LoRAs in transit (lz4, zstd)
  • Deduplicate identical LoRAs across users
  • Block-level deduplication for similar LoRAs

4. Multi-Cloud Strategy

  • Replicate LoRAs across multiple cloud providers
  • Automatic failover on cloud provider outages
  • Cost optimization: Store in cheapest region

Security & Compliance Considerations

File Validation

Upload-Time Checks:

  1. File format validation (magic bytes check)
  2. File size limits (e.g., max 2GB)
  3. Malware scanning (integrate ClamAV or VirusTotal API)
  4. SafeTensors format verification (ensure no embedded code)

Runtime Checks: 5. Signature verification (optional: sign LoRAs with user's key) 6. Checksum validation (compare with stored hash)

Access Control

User Isolation:

  • User LoRAs only accessible by owning user
  • API enforces user_id matching on all operations
  • Machine workers verify job's user_id before downloading

Sharing Permissions (Future):

  • flat_file.rel_type = "shared_lora" for public LoRAs
  • lora_permissions table for fine-grained sharing
  • Read-only access for shared LoRAs (no modification)

Data Retention

User Deletion:

  • On user account deletion, mark all LoRAs as hidden = true
  • Async job deletes LoRA files from cloud storage within 30 days
  • Machine caches evict on next cleanup cycle

Audit Logging:

  • Log all LoRA uploads, downloads, deletions
  • Track usage in OpenTelemetry spans
  • Compliance reports for data retention policies

Conclusion

This investigation confirms that EmProps has a strong foundation for user LoRA support:

Strengths:

  • ✅ Existing EmProps_Lora_Loader infrastructure
  • ✅ Robust model cache management with LRU eviction
  • flat_file storage system ready to use
  • ✅ Cloud storage handlers for AWS/GCS/Azure

Implementation Effort:

  • Phase 1 (Upload API): 1 week
  • Phase 2 (Enhanced Loader): 1 week
  • Phase 3 (Cache Management): 1 week
  • Phase 4 (UI Integration): 1-2 weeks
  • Phase 5 (Monitoring): 1 week
  • Total: 5-6 weeks for full production-ready implementation

Recommended Next Steps:

  1. ✅ Review this report with stakeholders
  2. ✅ Create implementation ADR with detailed technical decisions
  3. ✅ Begin Phase 1: Upload API and quota management
  4. ✅ Iterate based on user feedback during beta testing

Key Success Factors:

  • Start simple: Basic upload/download in Phase 1
  • Iterate quickly: Get user feedback early
  • Monitor closely: Cache hit rates and eviction frequency
  • Scale gradually: Expand quotas as storage costs stabilize

Document Version: 1.0 Last Updated: 2025-11-13 Author: EmProps Engineering Team Status: Ready for Implementation Planning

Released under the MIT License.