# Technical Architecture Report (Current State) Date: 2026-07-31 Scope: Current implementation only (no redesign suggestions) --- ## 1. Database Authoritative schema source: [prisma/schema.prisma](prisma/schema.prisma) ### Full Prisma Schema The full schema is defined in [prisma/schema.prisma](prisma/schema.prisma). ### Models related to requested domains - Users/Auth - User - Account - Session - VerificationToken - ClientAccess - Projects/Episodes - Project - EpisodeDueDate - Client - Shots/Tasks/Versions/Reviews - Shot - ShotGroup - Task - Version - Comment - CommentReply - Annotation - Approval - ReviewSession - Files/Storage - FootagePlate - ShotReference - SystemConfig - Delivery/Export adjacent - Shot fields: highResKey, highResFilename, exrOutput, shotVersion - Version fields: fileUrl, fileName, proxyUrl, thumbnailUrl, posterUrl ### Relationship summary - Client 1:N Project - Project 1:N Shot - Project 1:N Task - Project 1:N ReviewSession - Project 1:N EpisodeDueDate - Shot 1:N Task - Shot 1:N Version - Shot 1:N FootagePlate - Shot 1:N ShotReference - Task 1:N Version - Version 1:N Comment - Version 1:N Annotation - Version 1:N Approval - Comment 1:N CommentReply - User has many assigned/created entities across shots, tasks, versions, comments, approvals ### Existing status enums From [prisma/schema.prisma](prisma/schema.prisma): - ProjectStatus: ACTIVE, ON_HOLD, COMPLETED, ARCHIVED - ShotStatus: WAITING, IN_PROGRESS, INTERNAL_REVIEW, READY_FOR_CLIENT, CLIENT_REVIEW, REVISIONS, COMPLETE - ShotApprovalStatus: PENDING, INTERNALLY_APPROVED, CLIENT_APPROVED - TaskStatus: TODO, IN_PROGRESS, INTERNAL_REVIEW, CLIENT_REVIEW, CHANGES, DONE - ApprovalStatus: PENDING_REVIEW, APPROVED, REJECTED, NEEDS_CHANGES - ReviewStatus: PENDING, INTERNAL_APPROVED, CLIENT_APPROVED, NEEDS_CHANGES, FINAL_APPROVED Notes: - There is no dedicated Delivery model. - There is no dedicated Export model. - Delivery/export state is represented by file pointers and shot/version metadata fields. --- ## 2. API ### 2.1 Shots #### External shot APIs - GET /api/ext/projects - URL: /api/ext/projects - Method: GET - Purpose: List projects for pipeline tools - Request body: None - Response: projects[] with id, name, code, showId, projectType, status, dates, _count - Source: [app/api/ext/projects/route.ts](app/api/ext/projects/route.ts) - GET /api/ext/projects/{projectCode}/episodes - URL: /api/ext/projects/{projectCode}/episodes - Method: GET - Purpose: List distinct episodes and optionally shot payloads per episode - Request body: None - Response: project + episodes[]; optional shots[] includes exrOutput/timecodes - Source: [app/api/ext/projects/[projectCode]/episodes/route.ts](app/api/ext/projects/%5BprojectCode%5D/episodes/route.ts) - GET /api/ext/projects/{projectCode}/shots - URL: /api/ext/projects/{projectCode}/shots - Method: GET - Purpose: List shots by project code with filters/pagination - Request body: None - Response: project + pagination + shots[] - Source: [app/api/ext/projects/[projectCode]/shots/route.ts](app/api/ext/projects/%5BprojectCode%5D/shots/route.ts) - GET /api/ext/shots - URL: /api/ext/shots - Method: GET - Purpose: Legacy listing by projectId - Request body: None - Response: shots[] + total - Source: [app/api/ext/shots/route.ts](app/api/ext/shots/route.ts) - POST /api/ext/shots - URL: /api/ext/shots - Method: POST - Purpose: Create shot from external tool (JSON or multipart thumbnail) - Request body: - projectId, scene - optional episode, description, artistId, priority, fps, frameStart, frameEnd, dueDate, thumbnailUrl, shotGroupName, shotCode - optional thumbnail file (multipart) - Response: created shot object - Source: [app/api/ext/shots/route.ts](app/api/ext/shots/route.ts) - GET /api/ext/shots/lookup - URL: /api/ext/shots/lookup - Method: GET - Purpose: Canonical shot lookup by shotCode (+ optional projectCode) - Request body: None - Response: shot object with project, artist, tasks, latest version, exrOutput, shotVersion, source/seq timecodes - Source: [app/api/ext/shots/lookup/route.ts](app/api/ext/shots/lookup/route.ts) - GET /api/ext/shots/{shotId} - URL: /api/ext/shots/{shotId} - Method: GET - Purpose: Shot detail by DB id, or byCode mode - Request body: None - Response: full shot detail with tasks/latest version/counts - Source: [app/api/ext/shots/[shotId]/route.ts](app/api/ext/shots/%5BshotId%5D/route.ts) - PATCH /api/ext/shots/{shotId} - URL: /api/ext/shots/{shotId} - Method: PATCH - Purpose: Update mutable shot field(s) from pipeline tools - Request body: shotVersion (v###) currently supported - Response: success + updated shot id/shotVersion - Source: [app/api/ext/shots/[shotId]/route.ts](app/api/ext/shots/%5BshotId%5D/route.ts) #### Internal shot APIs - GET /api/shots/{shotId} - URL: /api/shots/{shotId} - Method: GET - Purpose: Internal dashboard shot detail payload - Request body: None - Response: shot + tasks + artists + permissions flags - Source: [app/api/shots/[shotId]/route.ts](app/api/shots/%5BshotId%5D/route.ts) - GET /api/projects/{projectId}/episodes - URL: /api/projects/{projectId}/episodes - Method: GET - Purpose: Internal distinct episode list for project - Request body: None - Response: episodes[] - Source: [app/api/projects/[projectId]/episodes/route.ts](app/api/projects/%5BprojectId%5D/episodes/route.ts) ### 2.2 Reviews - GET /api/review-sessions - Purpose: list review sessions - POST /api/review-sessions - Purpose: create review session token and portal link - DELETE /api/review-sessions - Purpose: deactivate review session - Source: [app/api/review-sessions/route.ts](app/api/review-sessions/route.ts) - POST /api/client/{token}/auth - Purpose: review password check + unlock cookie - Source: [app/api/client/[token]/auth/route.ts](app/api/client/%5Btoken%5D/auth/route.ts) - GET /api/client/{token}/project - Purpose: client portal project payload (shared shots/versions) - Source: [app/api/client/[token]/project/route.ts](app/api/client/%5Btoken%5D/project/route.ts) - GET /api/client/{token}/versions/{versionId} - Purpose: client review detail payload - Source: [app/api/client/[token]/versions/[versionId]/route.ts](app/api/client/%5Btoken%5D/versions/%5BversionId%5D/route.ts) - POST /api/client/{token}/comment - Purpose: client frame comment - Request body: versionId, frameNumber, timestamp, text - Response: created comment - Source: [app/api/client/[token]/comment/route.ts](app/api/client/%5Btoken%5D/comment/route.ts) - POST /api/client/{token}/annotation - Purpose: client annotation write - Request body: versionId, frameNumber, drawingData, optional color - Response: annotation - Source: [app/api/client/[token]/annotation/route.ts](app/api/client/%5Btoken%5D/annotation/route.ts) - POST /api/client/{token}/approve - Purpose: shot-level approve/changes and legacy version-level approval - Request body: - shot mode: shotId + action - version mode: versionId + status + notes - Response: success - Source: [app/api/client/[token]/approve/route.ts](app/api/client/%5Btoken%5D/approve/route.ts) - GET /api/versions/{versionId}/comments - Purpose: internal comments read - Source: [app/api/versions/[versionId]/comments/route.ts](app/api/versions/%5BversionId%5D/comments/route.ts) - GET /api/versions/{versionId}/annotations - Purpose: internal annotations read - Source: [app/api/versions/[versionId]/annotations/route.ts](app/api/versions/%5BversionId%5D/annotations/route.ts) - GET /api/playlist - Purpose: latest version per shot playlist - Source: [app/api/playlist/route.ts](app/api/playlist/route.ts) ### 2.3 File uploads - POST /api/upload - Purpose: authenticated upload to Hetzner via app server - Body: multipart file (+ type) - Response: url, key - Source: [app/api/upload/route.ts](app/api/upload/route.ts) - POST /api/upload/local - Purpose: authenticated video upload path - Body: multipart file - Response: url, key - Source: [app/api/upload/local/route.ts](app/api/upload/local/route.ts) - POST /api/upload/presign - Purpose: direct browser->object-storage upload URL - Body: fileName, contentType, optional folder - Response: presignedUrl, key, url - Source: [app/api/upload/presign/route.ts](app/api/upload/presign/route.ts) - GET/POST /api/uploadthing - Purpose: UploadThing route handler passthrough (if configured) - Source: [app/api/uploadthing/route.ts](app/api/uploadthing/route.ts) - POST /api/batch-upload/presign - Purpose: presign high-res upload key - Body: fileName - Response: presignedUrl, key - Source: [app/api/batch-upload/presign/route.ts](app/api/batch-upload/presign/route.ts) - POST /api/batch-upload/preview - Purpose: classify upload actions before upload - Body: projectId, fileNames[] - Response: items[] with statuses (new-version, rename-and-upload, create-task, update-highres, etc) - Source: [app/api/batch-upload/preview/route.ts](app/api/batch-upload/preview/route.ts) - POST /api/batch-upload/upload - Purpose: commit highres key or create version records - Body: - update-highres: action, shotId, projectId, key, fileName - version upload: action, shotId, projectId, file, task routing fields - Response: success + action result - Source: [app/api/batch-upload/upload/route.ts](app/api/batch-upload/upload/route.ts) ### 2.4 EXRs / Rendering / Delivery / Storage / Metadata - GET /api/files/{...key} - Purpose: file serving, range requests, local/Hetzner routing - Source: [app/api/files/[...key]/route.ts](app/api/files/%5B...key%5D/route.ts) - GET/POST /api/admin/migration - Purpose: local uploads migration status and per-key migration to Hetzner - Source: [app/api/admin/migration/route.ts](app/api/admin/migration/route.ts) - POST/DELETE /api/storage-test - Purpose: upload test object / delete test object - Source: [app/api/storage-test/route.ts](app/api/storage-test/route.ts) - POST/DELETE /api/shots/{shotId}/highres - Purpose: upload/remove high-res deliverable on shot - Source: [app/api/shots/[shotId]/highres/route.ts](app/api/shots/%5BshotId%5D/highres/route.ts) - GET /api/shots/{shotId}/highres/download - Purpose: internal presigned download URL for high-res - Source: [app/api/shots/[shotId]/highres/download/route.ts](app/api/shots/%5BshotId%5D/highres/download/route.ts) - GET /api/client/{token}/shots/{shotId}/highres/download - Purpose: client token-gated presigned high-res download URL - Source: [app/api/client/[token]/shots/[shotId]/highres/download/route.ts](app/api/client/%5Btoken%5D/shots/%5BshotId%5D/highres/download/route.ts) - GET/POST/DELETE /api/shots/{shotId}/references - Purpose: shot reference image management - Source: [app/api/shots/[shotId]/references/route.ts](app/api/shots/%5BshotId%5D/references/route.ts) - GET /api/storyboard/pdf - Purpose: server-rendered printable storyboard HTML with metadata options - Source: [app/api/storyboard/pdf/route.ts](app/api/storyboard/pdf/route.ts) Notes: - No dedicated REST endpoint that runs ffmpeg in this repository. - No dedicated REST endpoint that performs EXR rendering on the server. - No dedicated deliveries API namespace. --- ## 3. Storage Primary storage abstraction: [lib/storage.ts](lib/storage.ts) ### Provider modes - local - uploadthing - s3 - r2 - b2 - minio ### Dedicated high-res object storage path - Hetzner object storage helper methods are used for high-res and presigned direct uploads. - Config source precedence: - SystemConfig table keys - env fallback ### Folder/key layout in object storage (current code paths) - videos/ - image/ - highres/ - storage-test/ ### Local storage - LOCAL_UPLOAD_DIR (default ./uploads) - Served via /api/files catch-all route ### File naming conventions - Key format: {folder}/{uuid}-{sanitized-file-name} - Sanitization done by sanitizeFileName to avoid URL/signature problems in object keys ### EXR locations Within web app/runtime: - EXR references are naming metadata (Shot.exrOutput) and file-serving pathing. Within DCC tooling docs/scripts: - AE/Nuke workflows target shared export roots and per-shot folders. ### Preview locations - Version media URLs stored in Version.fileUrl - Accessed through app player routes and /api/files routing where applicable ### Thumbnail locations - Shot.thumbnailUrl - Version.thumbnailUrl - Can point to /api/files/{key} or external provider URL Relevant sources: - [lib/storage.ts](lib/storage.ts) - [app/api/files/[...key]/route.ts](app/api/files/%5B...key%5D/route.ts) - [actions/settings.ts](actions/settings.ts) --- ## 4. Export Pipeline (Current) ### AE panel Documented in repo: - [VFXReviewConnector.md](VFXReviewConnector.md) - [EXT_API_REFERENCE.md](EXT_API_REFERENCE.md) Implemented panel script also exists on the host AE installation (outside workspace), and behavior aligns with docs: - Episodes/shots discovery via ext APIs - Shot lookup API usage for metadata - Overlay/slate essential property updates (burn-in style overlays) - Queue EXR / Queue MP4 / Queue MOV render queue actions - Queue EXR (review convention) - Import exported EXR and import renders - Pull picture lock using seq timecodes - Increment shot version by PATCH call - Delivery prep that copies/renames EXR files to delivery folder convention ### External API endpoints used by DCC tools - GET /api/ext/projects/{projectCode}/episodes - GET /api/ext/projects/{projectCode}/shots - GET /api/ext/shots/lookup - PATCH /api/ext/shots/{shotId} ### Existing render scripts and pipelines - AE panel render queue automation (external script) - Nuke connector script creating write-node outputs and shot scripts: - [VFXReviewConnector.py](VFXReviewConnector.py) ### ffmpeg scripts - None found in this repository. ### Proxy generation - Version.proxyUrl field exists in schema. - No implemented proxy-generation worker/function found. ### Thumbnail generation - Upload/assignment flows exist. - No server-side frame-extract thumbnail generator found. ### Metadata extraction - EDL / picture-tracker CSV metadata parsing implemented: - [lib/edl-utils.ts](lib/edl-utils.ts) - [actions/shots.ts](actions/shots.ts) ### Burn-in generation - Achieved via DCC overlay/slate layers and essential properties in AE/Nuke tool workflows. - No ffmpeg burn-in path found. --- ## 5. Review System ### Current review workflow Core status derivation: - [lib/shot-status.ts](lib/shot-status.ts) Server action orchestration: - [actions/versions.ts](actions/versions.ts) - [actions/approvals.ts](actions/approvals.ts) - [actions/tasks.ts](actions/tasks.ts) - [actions/shots.ts](actions/shots.ts) - [actions/comments.ts](actions/comments.ts) ### Internal review - Version upload: - marks previous versions non-latest - creates new latest version - moves Task to INTERNAL_REVIEW - recalculates Shot.status ### Client review - Tokenized ReviewSession links - Optional password gate with signed cookie unlock - Client comment/annotation/approval endpoints - Share/unshare semantics via shot and version visibility fields ### Task status flow Observed statuses: - TODO - IN_PROGRESS - INTERNAL_REVIEW - CLIENT_REVIEW - CHANGES - DONE ### Shot status flow Derived in priority order: - REVISIONS (any task CHANGES) - COMPLETE (shotApprovalStatus CLIENT_APPROVED) - CLIENT_REVIEW / READY_FOR_CLIENT (internally approved + share flag) - IN_PROGRESS (task TODO/IN_PROGRESS) - INTERNAL_REVIEW (tasks exist) - WAITING (no tasks) ### Approval process - Version-level approvals create Approval rows and update Version.approvalStatus - Shot-level client actions supported via client approve endpoint and shot actions Reference doc in repo: - [Shot task workflow.md](Shot%20task%20workflow.md) --- ## 6. Authentication ### External tools - /api/ext/* routes authenticate using API_SECRET_KEY via: - Authorization: Bearer - x-api-key header ### Client review links - tokenized route with ReviewSession lookup - optional password hash validation and signed unlock cookie ### App users - NextAuth credentials provider - bcrypt password hash compare - JWT session strategy ### Middleware behavior - Route classes allowed through middleware auth gate: - /api/ext/ - /api/client/ - /api/display/ - /api/files/ - /api/uploadthing - Per-route auth still enforced in handlers Sources: - [auth.ts](auth.ts) - [auth.config.ts](auth.config.ts) - [middleware.ts](middleware.ts) - [lib/review-auth.ts](lib/review-auth.ts) --- ## 7. Existing Background Jobs ### Cron jobs - None found. ### Queues - None found. ### Workers - None found. ### Polling services - Display devices poll: - /api/display/events - /api/dashboard/stats ### Docker containers - vfxreview app container - postgres container - Source: [docker-compose.yml](docker-compose.yml) ### Scheduled tasks - Startup migration commands in container entrypoint: - [entrypoint.sh](entrypoint.sh) - AE panel Prep Delivery launches background PowerShell copy scripts on workstation (outside web app runtime) --- ## 8. After Effects Integration (Implemented Features) Repo-level references: - [VFXReviewConnector.md](VFXReviewConnector.md) - [EXT_API_REFERENCE.md](EXT_API_REFERENCE.md) Implemented capabilities observed/documented: - API calls - episodes/shots listing and shot lookup - shot version PATCH - Authentication - bearer API token in script - Comp discovery - shot code extraction from comp names and dropdown selection - Render queue integration - EXR Sequence - REVIEW_PREVIEW (MP4) - 4444 Tri (MOV) - EXR review sequence queue variant - Output path generation - shot/version naming conventions + export root paths - Shot lookup - uses /api/ext/shots/lookup for metadata and naming decisions - Upload features - no direct upload-to-web API flow in panel docs/script (render/output handled in DCC/filesystem) - Burn-in generation - overlay and slate essential properties - Review integration - preview comp build and version increment sync --- ## 9. Configuration ### Environment variables Primary reference: [.env.example](.env.example) Observed vars in code/docs include: - DATABASE_URL - NEXTAUTH_SECRET - NEXTAUTH_URL - NEXT_PUBLIC_APP_URL - NEXT_PUBLIC_APP_NAME - API_SECRET_KEY - AUTH_SECRET - STORAGE_PROVIDER - LOCAL_UPLOAD_DIR - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY - AWS_REGION - AWS_BUCKET_NAME - R2_ACCESS_KEY_ID - R2_SECRET_ACCESS_KEY - R2_ACCOUNT_ID - R2_BUCKET_NAME - R2_PUBLIC_URL - B2_APPLICATION_KEY_ID - B2_APPLICATION_KEY - B2_BUCKET_NAME - B2_ENDPOINT - MINIO_ENDPOINT - MINIO_ACCESS_KEY - MINIO_SECRET_KEY - MINIO_BUCKET_NAME - HETZNER_ENDPOINT - HETZNER_ACCESS_KEY - HETZNER_SECRET_KEY - HETZNER_BUCKET_NAME - UPLOADTHING_SECRET - UPLOADTHING_APP_ID - EMAIL_FROM - EMAIL_SERVER_HOST - EMAIL_SERVER_PORT - EMAIL_SERVER_USER - EMAIL_SERVER_PASSWORD - SLACK_DEFAULT_WEBHOOK ### Storage configuration - Provider abstraction in [lib/storage.ts](lib/storage.ts) - Hetzner overrides in [actions/settings.ts](actions/settings.ts) and SystemConfig table ### Render configuration - No server-side renderer configuration module found. - DCC render templates are documented in [VFXReviewConnector.md](VFXReviewConnector.md). ### ffmpeg configuration - None found in repository. ### Object storage configuration - Implemented for AWS S3/R2/B2/MinIO + dedicated Hetzner helper path --- ## 10. Existing Utility Functions (Reusable) ### Metadata extraction - parseEdlCsv - parsePictureTrackerCsv - Source: [lib/edl-utils.ts](lib/edl-utils.ts) ### File scanning - Local upload tree walker in migration route - Nuke connector plate/render directory scanners - Sources: - [app/api/admin/migration/route.ts](app/api/admin/migration/route.ts) - [VFXReviewConnector.py](VFXReviewConnector.py) ### EXR sequence detection - Nuke connector helper sequence detectors and pattern conversion - Source: [VFXReviewConnector.py](VFXReviewConnector.py) ### MOV generation - No server-side MOV generation utility found. - MOV outputs are DCC render queue outputs in external scripts/docs. ### Checksums - No checksum utility found. ### Frame counting - durationToFrameCount and frame math helpers - Source: [lib/frame-utils.ts](lib/frame-utils.ts) ### Timecode extraction/conversion - frameToTimecode / formatTimecode - CSV timecode validation/parsing - Sources: - [lib/frame-utils.ts](lib/frame-utils.ts) - [lib/utils.ts](lib/utils.ts) - [lib/edl-utils.ts](lib/edl-utils.ts) --- ## 11. High-Level Architecture Diagram ```mermaid flowchart LR A[AE Panel / Nuke Connector\nExternal DCC tools] -->|API key auth| B[Next.js App Router APIs] B --> C[Prisma ORM] C --> D[(PostgreSQL)] B --> E[Storage Abstraction] E --> F[(Hetzner Object Storage)] E --> G[(S3/R2/B2/MinIO)] B --> H[(Local uploads dir)] I[Internal reviewers\nNextAuth session] --> B J[Client reviewers\nToken review sessions] --> B K[ESP32 display client] -->|x-display-key polling| B A --> L[Shared filesystem render roots\nEXR/MP4/MOV outputs] L -->|served/linked via app metadata| B ``` --- ## 12. Files Inspected (Primary) - [prisma/schema.prisma](prisma/schema.prisma) - [lib/storage.ts](lib/storage.ts) - [lib/shot-status.ts](lib/shot-status.ts) - [lib/review-auth.ts](lib/review-auth.ts) - [lib/edl-utils.ts](lib/edl-utils.ts) - [lib/frame-utils.ts](lib/frame-utils.ts) - [lib/utils.ts](lib/utils.ts) - [auth.ts](auth.ts) - [auth.config.ts](auth.config.ts) - [middleware.ts](middleware.ts) - [next.config.ts](next.config.ts) - [.env.example](.env.example) - [docker-compose.yml](docker-compose.yml) - [Dockerfile](Dockerfile) - [entrypoint.sh](entrypoint.sh) - [EXT_API_REFERENCE.md](EXT_API_REFERENCE.md) - [VFXReviewConnector.md](VFXReviewConnector.md) - [VFXReviewConnector.py](VFXReviewConnector.py) - [# VFXReview Connector for Nuke.md](#%20VFXReview%20Connector%20for%20Nuke.md) - API handlers under [app/api](app/api) - Server actions under [actions](actions) --- End of current-state report.