Skip to main content

Backend API Reference

.NET 9.0 Web API built with Clean Architecture and Domain-Driven Design (DDD) principles.

For AI Agents

This documentation is designed for AI agents to quickly understand the backend structure and locate code.

Base URL

  • Development: https://localhost:5009
  • Docker (HTTPS): https://localhost:8443

Quick Reference

Model Upload & Management (10 endpoints)

MethodEndpointDescription
POST/modelsUpload new 3D model
POST/models/{modelId}/filesAdd file to existing model
POST/models/{modelId}/tagsUpdate model tags/description
GET/modelsList all models (?packId, ?projectId, ?textureSetId, ?page, ?pageSize)
GET/models/{id}Get model details
GET/models/{id}/fileDownload model file
PUT/models/{id}/default-texture-setSet default texture for version
POST/models/{id}/active-version/{versionId}Set active version
DELETE/models/{id}Soft delete model
DELETE/models/{modelId}/versions/{versionId}Soft delete version

Model Versions (7 endpoints)

MethodEndpointDescriptionAuth
POST/models/{modelId}/versionsCreate new versionNone
POST/models/{modelId}/versions/{versionId}/filesAdd file to versionNone
GET/models/{modelId}/versionsList all versionsNone
GET/models/{modelId}/versions/{versionId}Get version detailsNone
GET/models/{modelId}/versions/{versionId}/fileDownload renderable file (falls back to any file if no renderable exists)None
GET/models/{modelId}/versions/{versionId}/files/{fileId}Download specific fileNone
PUT/model-versions/{versionId}/material-namesSet material names extracted from 3D modelX-Api-Key

Material Names: The PUT /model-versions/{versionId}/material-names endpoint accepts { materialNames: string[] } and is called by the asset processor after extracting material names from the 3D model file. Material names are stored as a PostgreSQL text[] column.

Variant (Preset) Management (3 endpoints)

MethodEndpointDescription
PUT/model-versions/{versionId}/main-variantSet main variant for thumbnail generation
POST/model-versions/{versionId}/variantsAdd a variant (preset) name
DELETE/model-versions/{versionId}/variants/{variantName}Remove a variant name and all its texture mappings

Variant Names: Variant (preset) names are now persisted as a text[] column (VariantNames) on ModelVersion, independent of texture mappings. This means empty presets (with no linked texture sets) survive. When a texture mapping is created with a new variant name, it is auto-registered. The mainVariantName is also auto-set to the first named variant when it hasn't been set yet.

Files (5 endpoints)

MethodEndpointDescriptionAuth
POST/filesUpload a file (auto-generates thumbnail previews)None
GET/files/{id}Download any file by ID (incl. soft-deleted)None
GET/files/{id}/previewDownload PNG preview (?channel=rgb|r|g|b)None
POST/files/{id}/preview/uploadUpload PNG preview for a file (used by worker)X-Api-Key
DELETE/files/{id}Soft-delete file (moves to Recycled Files)None

Note: GET /files/{id} and GET /files/{id}/preview serve both active and soft-deleted files, enabling thumbnail display in the Recycled Files view. The same applies to model and model version thumbnail endpoints.

Thumbnails - Models (4 endpoints)

MethodEndpointDescriptionAuth
GET/models/{id}/thumbnailGet thumbnail statusNone
POST/models/{id}/thumbnail/regenerateQueue thumbnail regenerationNone
POST/models/{id}/thumbnail/uploadUpload custom thumbnailX-Api-Key
GET/models/{id}/thumbnail/fileDownload thumbnail imageNone

Thumbnails - Texture Sets (5 endpoints)

MethodEndpointDescriptionAuth
POST/texture-sets/{id}/thumbnail/uploadUpload WebP thumbnail for texture setX-Api-Key
POST/texture-sets/{id}/thumbnail/png-uploadUpload PNG thumbnail for texture setX-Api-Key
GET/texture-sets/{id}/thumbnail/fileDownload WebP thumbnail imageNone
GET/texture-sets/{id}/thumbnail/png-fileDownload PNG thumbnail imageNone
POST/texture-sets/{id}/thumbnail/regenerateQueue texture set thumbnail regeneration (body: { uvScale?, geometryType?, proxySize? })None

Texture Sets (17 endpoints)

MethodEndpointDescription
GET/texture-setsList all texture sets (?packId, ?projectId, ?kind, ?page, ?pageSize)
GET/texture-sets/{id}Get texture set details (includes kind, tilingScaleX/Y, uvMappingMode, uvScale)
GET/texture-sets/by-file/{fileId}Get texture set containing a file
POST/texture-setsCreate new texture set (JSON, optional kind: 0=ModelSpecific, 1=Universal)
POST/texture-sets/with-fileCreate texture set with file upload (?kind query param)
PUT/texture-sets/{id}Update texture set
PUT/texture-sets/{id}/kindChange texture set kind (0=ModelSpecific, 1=Universal); auto-enqueues thumbnail generation when changed to Universal
PUT/texture-sets/{id}/tiling-scaleUpdate tiling scale + UV mapping (Universal only; optional uvMappingMode, uvScale)
PUT/texture-sets/{setId}/textures/{textureId}/web-proxyUpload web proxy for a texture (?size query param, multipart file). Auth: X-Api-Key
DELETE/texture-sets/{id}Soft delete texture set
DELETE/texture-sets/{id}/hardHard delete (keeps files)
POST/texture-sets/{id}/texturesAdd texture to set
DELETE/texture-sets/{packId}/textures/{textureId}Remove texture from set
PUT/texture-sets/{setId}/textures/{textureId}/typeChange texture type
POST/texture-sets/{packId}/model-versions/{modelVersionId}Associate with model version (?materialName)
DELETE/texture-sets/{packId}/model-versions/{modelVersionId}Disassociate from model version (?materialName)
POST/texture-sets/{packId}/models/{modelId}/all-versionsAssociate with all model versions (?materialName)

Material Slot Mapping

Texture set association endpoints accept optional ?materialName and ?variantName query parameters to link a texture set to a specific material slot and preset/variant. When omitted, both default to empty string (applies to all materials / Default preset). Each (ModelVersionId, TextureSetId, MaterialName, VariantName) combination forms a unique mapping via the ModelVersionTextureSets join table with a composite primary key. For named materials, only one texture set can be assigned per material name per variant. Variant names are persisted in the VariantNames text[] column on ModelVersion and are auto-registered when creating texture mappings with a new variant name. The mainVariantName on ModelVersion tracks which preset is active for thumbnail generation and is auto-set to the first named variant when not yet configured.

Recycled Files (4 endpoints)

MethodEndpointDescription
GET/recycledList all recycled items
POST/recycled/{entityType}/{entityId}/restoreRestore recycled item
GET/recycled/{entityType}/{entityId}/previewPreview delete impact
DELETE/recycled/{entityType}/{entityId}/permanentPermanently delete

Worker API - Thumbnail Jobs (4 endpoints)

MethodEndpointDescription
POST/thumbnail-jobs/dequeueDequeue next job (workers only)
POST/thumbnail-jobs/{jobId}/completeMark model job complete (workers only)
POST/thumbnail-jobs/texture-sets/{jobId}/finishMark texture set job complete/failed
POST/test/thumbnail-complete/{modelId}Test completion notification (dev)

Settings (4 endpoints)

MethodEndpointDescription
GET/settingsGet application settings (includes textureProxySize)
GET/settings/allGet all settings (key-value)
GET/settings/blender-enabledGet Blender integration status ({ enableBlender: bool })
PUT/settingsUpdate application settings (includes textureProxySize: 256/512/1024/2048)

Blender / WebDAV (5 virtual endpoints)

Handled by WebDavMiddleware — not standard REST endpoints. Requires ENABLE_BLENDER=true environment variable.

MethodPathDescription
LOCK/modelibr/Models/{name}.blendSynthetic lock for macOS Finder / Windows pre-PUT flow — returns token (201)
UNLOCK/modelibr/Models/{name}.blendReleases synthetic lock — returns 204
PUT/modelibr/Models/{name}.blendCreate new model from .blend file (returns 201 or 403 if disabled)
PUT/modelibr/Models/{name}/newestVersion.blend@Upload temp .blend file (Blender Safe Save step 1)
MOVE/modelibr/Models/{name}/newestVersion.blend@.blendCreate new version from temp file (Blender Safe Save step 2)

Blender Safe Save flow: Blender writes to a temp file (newestVersion.blend@), then renames (MOVE) it to the final path. The middleware intercepts the MOVE, computes the hash, and creates a new model version via CreateModelVersionCommand if the content changed. A ModelUploadedEvent is dispatched to trigger the asset-processor's .blend → .glb conversion and thumbnail generation.

Multi-file drop (macOS Finder / Windows Explorer): When a user drops multiple .blend files onto the mounted WebDAV Models folder, macOS Finder sends a LOCK before each PUT. The middleware intercepts LOCK/UNLOCK for {name}.blend paths and returns a synthetic lock token so the NWebDav library cannot block concurrent uploads. A 0-byte PUT guard skips model creation and returns 201 so the client does not retry; only PUTs with actual content create a model.

REST API .blend support: POST /models and POST /models/{modelId}/versions also accept .blend files. The ModelUploadedEvent is dispatched for both renderable and project (.blend) file types, triggering the asset-processor pipeline.

Total: 58 endpoints (53 REST + 5 WebDAV)

Pagination

List endpoints (/models, /texture-sets, /sprites, /sounds) support optional server-side pagination via query parameters:

ParameterTypeDescription
pageintPage number (1-based). Required with pageSize.
pageSizeintItems per page. Required with page.

When paginated (both page and pageSize provided), returns:

{
"items": [...],
"totalCount": 100,
"page": 1,
"pageSize": 20,
"totalPages": 5
}

When not paginated (params omitted), returns a flat array for backward compatibility. WebDAV and other internal consumers rely on this.

Architecture Overview

The backend follows Clean Architecture with strict layer separation:

WebApi → Application → Domain ← Infrastructure
↓ ↑
SharedKernel ← ← ← ← ↑

Key Files

WhatWhere
API Endpointssrc/WebApi/Endpoints/*.cs
Commands/Queriessrc/Application/*/
Domain Entitiessrc/Domain/Models/*.cs
Repository Interfacessrc/Application/Abstractions/Repositories/
Repository Implementationssrc/Infrastructure/Data/Repositories/
DI Registrationsrc/*/DependencyInjection.cs

Texture Types

Albedo, Normal, Metallic, Roughness, AmbientOcclusion, Emissive, Height, Opacity

File Upload Limits

  • Kestrel MaxRequestBodySize: 1 GB (configured in Program.cs)
  • FormOptions MultipartBodyLengthLimit: 1 GB (configured in Program.cs)
  • nginx client_max_body_size: 1024 MB (configured in nginx/nginx.conf)
  • Application-level validation: Configured via Settings page, stored in database (maxFileSizeBytes)

Files are stored using hash-based deduplication (HashBasedFileStorage). When permanently deleting entities, orphaned File records and physical files are cleaned up if no other entity references the same hash.

Error Handling

Errors are returned as JSON with error.code and error.message.

Read-Only Query Pattern for Specialized Components

Specialized infrastructure components (WebDAV, Blender addon layer) that need complex data graphs should query DbContext directly rather than using shared repositories.

Why?

  • Repositories stay lean - Frontend calls don't over-fetch data
  • Specialized queries - Each component crafts exactly the includes it needs
  • Read-only optimization - Use AsNoTracking() for performance
  • Self-contained - Optimizations don't affect other consumers

Pattern

// In specialized infrastructure component (e.g., VirtualAssetStore)
private async Task<Project?> GetProjectWithFullAssetGraph(IServiceProvider sp, string name)
{
var dbContext = sp.GetRequiredService<ApplicationDbContext>();

return await dbContext.Projects
.AsNoTracking() // Read-only, no change tracking
.Include(p => p.Models)
.ThenInclude(m => m.Versions)
.ThenInclude(v => v.Files)
.Include(p => p.Sounds)
.ThenInclude(s => s.File)
.AsSplitQuery() // Prevent cartesian explosion
.FirstOrDefaultAsync(p => p.Name == name);
}

When to Use

Use CaseApproach
Frontend API endpointsUse repositories (lean queries)
WebDAV virtual file systemDirect DbContext with full includes
Blender addon data exportDirect DbContext with full includes
Write operationsAlways use repositories

Key Rules

  1. Read-only access only - Never modify entities fetched this way
  2. Always use AsNoTracking() - Since we're not tracking changes
  3. Use AsSplitQuery() - When including multiple collections to avoid cartesian products
  4. Keep in Infrastructure layer - This pattern is for Infrastructure components, not Application layer