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_filetable - ❌ 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_filetable withtags=['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_Loadernode
Table of Contents
- Current Infrastructure
- Requirements
- Proposed Architecture
- Implementation Plan
- Database Schema Changes
- Machine Cache Management
- API Endpoints
- ComfyUI Integration
- Success Metrics
- 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 LoraLoaderLimitation: 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_usedtimestamp anduse_count - Protected models (won't be evicted)
- Ignored models (
is_ignoreflag for system models) - Disk space management with
min_free_space_gbsetting get_least_recently_used_models()for cache eviction
Database Schema:
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
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
User LoRA Upload
- Users can upload
.safetensorsor.ptLoRA 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)
- Users can upload
LoRA Discovery
- Users can browse their uploaded LoRAs
- Search by name, tags, upload date
- Display file size, upload date, usage count
LoRA Usage in Workflows
- Workflows can reference user's LoRAs by
flat_file.id EmProps_Lora_Loaderenhanced to support user LoRAs- Workflow JSON stores LoRA reference:
{"lora_source": "user", "lora_id": "123"}
- Workflows can reference user's LoRAs by
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
Quota Management
- Per-user LoRA storage quota (e.g., 10GB per user)
- Admin configurable quotas
- Warning when approaching quota limit
Non-Functional Requirements
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
Reliability
- Graceful fallback if user LoRA fails to download
- Retry mechanism with exponential backoff
- Clear error messages to user
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)
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}.ptMachine 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.safetensorsDatabase Storage (PostgreSQL):
-- 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):
-- 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:
✅ Create API endpoint:
POST /api/loras/upload- Accept
.safetensorsand.ptfiles - 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_filerecord withtags=['lora']
- Accept
✅ 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
- List user's LoRAs from
✅ Create API endpoint:
DELETE /api/loras/:id- Soft delete from
flat_file(sethidden = true) - Delete from cloud storage (async job)
- Invalidate machine caches (via Redis pub/sub)
- Soft delete from
✅ 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
- Query total LoRA size per user:
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_filerecords 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:
✅ Enhance
EmProps_Lora_Loadernode- Add
lora_sourceinput:["shared", "user"] - For
usersource, acceptflat_file_idinstead of filename - Query
flat_filetable to get URL - Download from user's cloud storage path
- Cache in
loras/users/{user_id}/directory
- Add
✅ Create
UserLoraDownloaderutility class- Fetch
flat_filerecord by ID - Verify user has access (via job's user_id context)
- Download from
flat_file.urlto local cache - Handle authentication tokens for cloud storage
- Track download progress (for UI feedback)
- Fetch
✅ Update workflow JSON schema
- Support LoRA references:
{"lora_source": "user", "lora_id": 123} - Backward compatible with existing
lora_namestring references
- Support LoRA references:
✅ 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 UserLoraDownloaderclass- 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:
✅ Add LoRA cache quota setting
- New
model_cache.dbsetting:lora_cache_max_gb(default: 50) - Separate from general model cache
- Configurable per machine via environment variable
- New
✅ Implement LRU eviction for user LoRAs
- Before downloading, check if cache exceeds quota
- Query
get_least_recently_used_models()filtered bymodel_type='user_lora' - Delete oldest LoRAs until space available
- Update
model_cache.dbafter deletions
✅ 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
✅ 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:
✅ 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
✅ LoRA upload component
- Drag-and-drop file upload
- Progress bar during upload
- Validation feedback (file format, size, quota)
- Success/error notifications
✅ 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
✅ 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:
✅ Add OpenTelemetry instrumentation
- Trace LoRA downloads (duration, size, source)
- Track cache hit/miss rates
- Monitor eviction events
- Alert on high download failure rates
✅ Create Dash0 dashboards
- LoRA cache hit rate by machine
- Average download time per LoRA size
- Eviction frequency and reasons
- User quota utilization
✅ 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
✅ 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:
-- 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:
{
"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
-- 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:
-- 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:
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
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_free2. Time-Based Eviction - Age-Based
Triggered: Daily cron job at 3 AM
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_bytes3. Quota-Based Eviction - User-Specific
Triggered: When user uploads new LoRA
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 TrueCache 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 # EvictableCache Statistics:
{
"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
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
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
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
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 cycle5. Get User Quota
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)
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:
@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:
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:
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 NoneWorkflow JSON Schema
User LoRA Reference:
{
"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):
{
"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:
- File format validation (magic bytes check)
- File size limits (e.g., max 2GB)
- Malware scanning (integrate ClamAV or VirusTotal API)
- 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_idmatching on all operations - Machine workers verify job's
user_idbefore downloading
Sharing Permissions (Future):
flat_file.rel_type = "shared_lora"for public LoRAslora_permissionstable 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_Loaderinfrastructure - ✅ Robust model cache management with LRU eviction
- ✅
flat_filestorage 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:
- ✅ Review this report with stakeholders
- ✅ Create implementation ADR with detailed technical decisions
- ✅ Begin Phase 1: Upload API and quota management
- ✅ 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
