file-upload-presign
File Upload — Presigned URL
Full design doc with flow diagrams:
apps/erify_api/docs/FILE_UPLOAD.md
Key Files
| Layer | Path |
|---|---|
| API contract | packages/api-types/src/uploads/schemas.ts |
| Backend service | apps/erify_api/src/uploads/upload.service.ts |
| Backend controller | apps/erify_api/src/uploads/upload.controller.ts |
| Shared browser upload utils | packages/browser-upload/src/index.ts |
| Compression worker | packages/browser-upload/src/image-compress.worker.ts |
| Frontend API utils | apps/erify_studios/src/features/tasks/api/presign-upload.ts |
| Frontend form | apps/erify_studios/src/components/json-form/json-form.tsx |
How It Works (Summary)
- Client calls
POST /uploads/presignwith{ use_case, mime_type, file_size, file_name, [task_id, field_key] } UploadServicevalidates MIME type and file size againstUSE_CASE_RULES- For
MATERIAL_ASSET: additionally validates against the task snapshot schema'sacceptrule for the givenfield_key - Returns a short-lived presigned PUT URL pointing to Cloudflare R2
- Client PUTs the file directly to R2 — never through the API server
Critical: The direct R2 PUT uses bare fetch(), not apiClient. Adding the API Authorization header causes R2 to return 403.
Use Cases & Limits
| Use Case | Max Size | Allowed MIME Types |
|---|---|---|
QC_SCREENSHOT |
200 KB | image/jpeg, image/png, image/webp |
SCENE_REFERENCE |
10 MB | image/jpeg, image/png, image/webp, application/pdf |
INSTRUCTION_ASSET |
50 MB | image/*, application/pdf, video/mp4 |
MATERIAL_ASSET |
50 MB | image/*, application/pdf, video/mp4 |
Defined in FILE_UPLOAD_USE_CASE_RULES in packages/api-types/src/uploads/schemas.ts. When changing limits, update this table and the design doc.
QC_SCREENSHOT 200 KB limit intentionally matches SCREENSHOT_MAX_BYTES (derived from getImageCompressionTargetBytes()) in json-form.tsx. Both must stay in sync.
MATERIAL_ASSET Routing Rules
The upload_routing metadata key is typed as UploadRoutingMetadata (exported from @eridu/api-types/uploads). Both TaskGenerationProcessor (producer) and UploadService.extractDirectoryFromMetadata (consumer) use this type to enforce the contract.
Storage directory is resolved in this priority order:
- Show-linked +
task.type === CLOSURE→ forcemc-review(legacy R2 directory name; not renamed to avoid storage migration) task.metadata.upload_routing.material_asset_directory— if present, use it directly- No show-linked target →
single-use - Show-linked +
task.type === SETUP→pre-production - Show-linked + any other type →
show-general
Special case: INSTRUCTION_ASSET (non-material use case) is currently routed to pre-production.
Frontend Image Compression
JsonForm compresses images before requesting a presign (for MATERIAL_ASSET only):
- Target:
min(field.validation.max_size, 200 KB) - Worker-first native compression (
Web Worker+OffscreenCanvas) with main-thread canvas fallback - Scale [1→0.6] and quality [0.9→0.34]
- Hard client-side size check after compression; throws before calling presign API if still too large
Frontend Submit Gating (JsonForm)
JsonForm submit flow is split into two explicit phases:
validateBeforeSubmit()validates the full form but ignores required-file errors only for file fields with pending uploads.flushPendingFileUploads()uploads pending files, writes resulting URL values back into form state, and returns final content for submit payloads.
Additional rules:
- Pending entries with
isPreparingorerrormust block submit. - Image preparation occurs on file selection; submit path should upload the prepared file already stored in pending state.
- Uploaded file URL cache (by per-field fingerprint
name:size:type:lastModified) can reuse URLs and skip duplicate uploads within one form session. - Keep upload cache across retries/partial-success uploads, and clear it only after successful submit API completion.
- Per-field cache entries should still be removed when that field file is replaced/cleared.
Checklist: Adding a New Use Case
- Add enum value to
FILE_UPLOAD_USE_CASEinpackages/api-types/src/uploads/schemas.ts - Add entry to
FILE_UPLOAD_USE_CASE_RULESinpackages/api-types/src/uploads/schemas.ts - Add routing logic in
resolveStorageUseCaseForObjectKeyinupload.service.tsif needed - Update the use case table in this skill and the design doc
- Add tests in
upload.service.spec.ts
Checklist: Changing a Size Limit
- Update
FILE_UPLOAD_USE_CASE_RULES[USE_CASE].max_file_size_bytesinpackages/api-types/src/uploads/schemas.ts - If
QC_SCREENSHOT: also verifySCREENSHOT_MAX_BYTESinjson-form.tsxstill matches (it's derived fromgetImageCompressionTargetBytes()) - Update the use case table above and in the design doc
- Run
pnpm --filter erify_api test --testPathPattern=upload
More from allenlin90/eridu-services
service-pattern-nestjs
Comprehensive NestJS service implementation patterns. This skill should be used when implementing Model Services, Orchestration Services, or business logic with NestJS decorators.
8erify-authorization
Patterns for implementing authorization in erify_api with current StudioMembership + AdminGuard behavior, plus planned RBAC references
6data-validation
Provides comprehensive guidance for input validation, data serialization, and ID management in backend APIs. This skill should be used when designing validation schemas, transforming request/response data, mapping database IDs to external identifiers, and ensuring type safety across API boundaries.
6code-quality
Provides general code quality and best practices guidance applicable across languages and frameworks. Focuses on linting, testing, and type safety.
6repository-pattern-nestjs
Comprehensive Prisma repository implementation patterns for NestJS. This skill should be used when implementing repositories that extend BaseRepository or use Prisma delegates.
6task-template-builder
Provides guidelines for the Task Template Builder architecture, including Schema alignment, Draft storage, Drag-and-Drop, and Validation logic.
6