feat: V2 microservices stack — backend services, gateway, JWT auth

Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-29 23:29:31 +03:30
parent 53ea78a00d
commit 90ac0b81d1
7636 changed files with 3707504 additions and 240 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+823
View File
@@ -0,0 +1,823 @@
openapi: 3.0.3
info:
title: FlatRender Render Orchestrator (internal)
version: 1.0.0
description: |
Render job orchestration, node pool management, snapshots, exports.
Owned by Go service. The browser connects to WebSocket via Gateway
for live progress.
servers:
- url: http://render-svc.internal/v1
security:
- BearerAuth: []
- ServiceToken: []
tags:
- name: Jobs
- name: Snapshots
- name: Exports
- name: Nodes
- name: Admin
- name: Internal
paths:
# ===================== JOBS =====================
/renders:
get:
tags: [Jobs]
summary: List user's render jobs
parameters:
- { name: status, in: query, schema: { type: string } }
- { name: page, in: query, schema: { type: integer } }
- { name: page_size, in: query, schema: { type: integer } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/RenderJob' } }
meta: { $ref: '#/components/schemas/PaginationMeta' }
post:
tags: [Jobs]
summary: Submit new render job
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/RenderJobCreate' }
responses:
'201':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderJob' }
'402': { description: Insufficient balance/charge }
/renders/{job_id}:
get:
tags: [Jobs]
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderJobDetail' }
/renders/{job_id}/cancel:
post:
tags: [Jobs]
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
content:
application/json:
schema:
type: object
properties:
reason: { type: string }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
cancelled: { type: boolean }
refund_amount_minor: { type: integer, format: int64 }
/renders/{job_id}/retry:
post:
tags: [Jobs]
summary: Retry a failed render (uses same config)
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'201':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderJob' }
/renders/{job_id}/progress:
get:
tags: [Jobs]
summary: |
Fallback polling endpoint when WebSocket isn't usable.
Browser primary is WS at /ws/v1/render/{job_id}.
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderProgress' }
/renders/{job_id}/logs:
get:
tags: [Jobs]
summary: Get render execution logs (admin or owner)
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
- { name: level, in: query, schema: { type: string, enum: [debug, info, warn, error] } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
logs:
type: array
items:
type: object
properties:
timestamp: { type: string, format: date-time }
level: { type: string }
node_id: { type: string, format: uuid, nullable: true }
message: { type: string }
meta: { type: object }
# ===================== SNAPSHOTS =====================
/snapshots:
post:
tags: [Snapshots]
summary: Request single-frame snapshot of a scene
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [saved_project_id, scene_key, frame_number]
properties:
saved_project_id: { type: string, format: uuid }
scene_key: { type: string }
frame_number: { type: integer, minimum: 0 }
responses:
'202':
description: Snapshot queued (or returned immediately if cached)
content:
application/json:
schema:
type: object
properties:
snapshot_id: { type: string, format: uuid }
status: { type: string, enum: [Cached, Pending, Rendering] }
image_url: { type: string, nullable: true }
thumbnail_url: { type: string, nullable: true }
expires_at: { type: string, format: date-time, nullable: true }
/snapshots/{snapshot_id}:
get:
tags: [Snapshots]
parameters:
- { name: snapshot_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/Snapshot' }
# ===================== EXPORTS =====================
/exports:
get:
tags: [Exports]
summary: List user's exports
parameters:
- { name: page, in: query, schema: { type: integer } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/Export' } }
meta: { $ref: '#/components/schemas/PaginationMeta' }
/exports/{export_id}:
get:
tags: [Exports]
parameters:
- { name: export_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/ExportDetail' }
delete:
tags: [Exports]
parameters:
- { name: export_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'204': { description: Deleted }
/exports/{export_id}/extend:
post:
tags: [Exports]
summary: Extend auto-delete date (paid feature)
parameters:
- { name: export_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
days: { type: integer, minimum: 1, maximum: 365 }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
new_auto_delete_date: { type: string, format: date-time }
/exports/{export_id}/download-url:
get:
tags: [Exports]
summary: Get presigned MinIO URL (short-lived)
parameters:
- { name: export_id, in: path, required: true, schema: { type: string, format: uuid } }
- { name: format, in: query, schema: { type: string, default: mp4 } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
url: { type: string }
expires_at: { type: string, format: date-time }
# ===================== NODES (admin) =====================
/nodes:
get:
tags: [Nodes]
summary: List nodes
parameters:
- { name: region, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/RenderNode' } }
post:
tags: [Nodes]
summary: (Admin) Register new node
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/RenderNodeCreate' }
responses:
'201':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderNode' }
/nodes/{node_id}:
get:
tags: [Nodes]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderNodeDetail' }
patch:
tags: [Nodes]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
priority: { type: integer }
is_active: { type: boolean }
accepts_new_jobs: { type: boolean }
node_kind: { type: string }
owner_user_id: { type: string, format: uuid }
next_maintenance_at: { type: string, format: date-time }
maintenance_reason: { type: string }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/RenderNode' }
/nodes/{node_id}/restart:
post:
tags: [Nodes]
summary: Reboot node OS
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'202': { description: Restart issued }
/nodes/{node_id}/release:
post:
tags: [Nodes]
summary: Force-release node from any current job
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'204': { description: Released }
/nodes/{node_id}/close-ae:
post:
tags: [Nodes]
summary: Force-kill AfterFX on a node
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'204': { description: AE closed }
/nodes/{node_id}/health:
get:
tags: [Nodes]
summary: Current node health
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/NodeHealth' }
/nodes/{node_id}/health/history:
get:
tags: [Nodes]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
- { name: from, in: query, schema: { type: string, format: date-time } }
- { name: to, in: query, schema: { type: string, format: date-time } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/NodeHealth' }
/nodes/{node_id}/crashes:
get:
tags: [Nodes]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/NodeCrash' } }
/node-updates:
get:
tags: [Nodes]
summary: Available node software updates
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/NodeUpdate' } }
/node-updates/{update_id}/rollout:
post:
tags: [Nodes]
summary: Trigger rollout to selected nodes
parameters:
- { name: update_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
node_ids: { type: array, items: { type: string, format: uuid } }
responses:
'202': { description: Rollout queued }
# ===================== INTERNAL (called by node agents) =====================
/internal/nodes/{node_id}/heartbeat:
post:
tags: [Internal]
summary: Node sends heartbeat (every 5s)
security: [NodeHmac: []]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NodeHeartbeat' }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
next_heartbeat_in_sec: { type: integer }
pending_commands:
type: array
description: e.g. "cancel current job", "update software"
items: { type: object }
/internal/nodes/{node_id}/online:
post:
tags: [Internal]
summary: Node agent reports it just started
security: [NodeHmac: []]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
node_agent_version: { type: string }
current_ae_version: { type: string }
available_ae_versions: { type: array, items: { type: string } }
ram_gb: { type: integer }
cpu_cores: { type: integer }
cache_used_gb: { type: integer }
cached_template_md5s: { type: array, items: { type: string } }
responses:
'200': { description: Acknowledged }
/internal/render/jobs/{job_id}/frames:
post:
tags: [Internal]
summary: Node pushes per-frame progress
security: [NodeHmac: []]
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [frame_job_id, frame_number]
properties:
frame_job_id: { type: string, format: uuid }
frame_number: { type: integer }
file_size_bytes: { type: integer }
completed_at: { type: string, format: date-time }
responses:
'204': { description: Recorded }
/internal/render/jobs/{job_id}/crash:
post:
tags: [Internal]
summary: Node reports an AE crash on this job
security: [NodeHmac: []]
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [node_id]
properties:
node_id: { type: string, format: uuid }
frame_job_id: { type: string, format: uuid }
last_known_frame: { type: integer }
crash_signal: { type: string }
ae_version: { type: string }
error_log_tail: { type: string }
log_file_url: { type: string }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
action_recommended:
type: string
enum: [ResetAndRestart, ReassignWork, RestartNode]
reassigned_to_node_id:
type: string
format: uuid
nullable: true
/internal/render/jobs/{job_id}/replica-ready:
post:
tags: [Internal]
summary: Node reports replica .aep saved (after JSX run)
security: [NodeHmac: []]
parameters:
- { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [node_id, replica_path]
properties:
node_id: { type: string, format: uuid }
replica_path: { type: string }
replica_md5: { type: string }
responses:
'204': { description: Acknowledged }
/internal/nodes/{node_id}/cache-update:
post:
tags: [Internal]
summary: Node reports a template cache change
security: [NodeHmac: []]
parameters:
- { name: node_id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [action, aep_file_md5]
properties:
action: { type: string, enum: [Downloaded, Evicted, Verified, Failed] }
project_id: { type: string, format: uuid }
aep_file_md5: { type: string }
file_size_bytes: { type: integer, format: int64 }
cache_used_gb: { type: integer }
error_message: { type: string }
responses:
'204': { description: Recorded }
components:
securitySchemes:
BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
ServiceToken: { type: http, scheme: bearer }
NodeHmac:
type: apiKey
in: header
name: X-Node-Signature
schemas:
PaginationMeta:
type: object
properties:
page: { type: integer }
page_size: { type: integer }
total: { type: integer }
has_more: { type: boolean }
RenderJob:
type: object
properties:
id: { type: string, format: uuid }
saved_project_id: { type: string, format: uuid }
name: { type: string }
step: { type: string }
render_progress: { type: integer }
priority_queue: { type: string }
price_type: { type: string }
paid_price_minor: { type: integer, format: int64 }
quality: { type: string }
resolution: { type: string }
frame_rate: { type: integer }
duration_sec: { type: number }
has_voiceover: { type: boolean }
image_preview_b64: { type: string, nullable: true }
failed_message: { type: string, nullable: true }
export_id: { type: string, format: uuid, nullable: true }
queued_at: { type: string, format: date-time }
started_at: { type: string, format: date-time, nullable: true }
completed_at: { type: string, format: date-time, nullable: true }
RenderJobCreate:
type: object
required: [saved_project_id, quality, resolution]
properties:
saved_project_id: { type: string, format: uuid }
quality: { type: string, enum: [Low, Medium, High, Full, Lossless] }
resolution: { type: string, enum: [HD, FullHD, TwoK, FourK] }
frame_rate: { type: integer, enum: [24, 25, 30, 60] }
is_60_fps: { type: boolean }
price_type:
type: string
enum: [Free, Preview, Cash, Plan, Snapshot, Reseller]
discount_code: { type: string }
support_flatrender: { type: boolean }
tell_me_when_done: { type: boolean }
preferred_region: { type: string }
RenderJobDetail:
allOf:
- $ref: '#/components/schemas/RenderJob'
- type: object
properties:
frame_jobs:
type: array
items: { $ref: '#/components/schemas/FrameJob' }
retry_count: { type: integer }
repair_attempts: { type: integer }
export: { $ref: '#/components/schemas/Export', nullable: true }
FrameJob:
type: object
properties:
id: { type: string, format: uuid }
node_id: { type: string, format: uuid }
start_frame: { type: integer }
end_frame: { type: integer }
order_value: { type: integer }
status: { type: string }
frames_rendered: { type: integer }
frames_validated: { type: integer }
attempt: { type: integer }
last_error: { type: string, nullable: true }
RenderProgress:
type: object
properties:
job_id: { type: string, format: uuid }
step: { type: string }
progress: { type: integer }
current_frame: { type: integer, nullable: true }
total_frames: { type: integer, nullable: true }
eta_seconds: { type: integer, nullable: true }
preview_b64: { type: string, nullable: true }
active_nodes: { type: integer }
message: { type: string, nullable: true }
Snapshot:
type: object
properties:
id: { type: string, format: uuid }
saved_project_id: { type: string, format: uuid }
scene_key: { type: string }
frame_number: { type: integer }
status: { type: string }
image_url: { type: string, nullable: true }
thumbnail_url: { type: string, nullable: true }
width: { type: integer, nullable: true }
height: { type: integer, nullable: true }
duration_ms: { type: integer, nullable: true }
expires_at: { type: string, format: date-time }
requested_at: { type: string, format: date-time }
completed_at: { type: string, format: date-time, nullable: true }
Export:
type: object
properties:
id: { type: string, format: uuid }
saved_project_id: { type: string, format: uuid }
path: { type: string }
image: { type: string }
size_bytes: { type: integer, format: int64 }
duration_sec: { type: number }
width: { type: integer }
height: { type: integer }
file_extension: { type: string }
file_type: { type: string }
render_quality: { type: string }
create_type: { type: string }
produce_date: { type: string, format: date-time }
auto_delete_date: { type: string, format: date-time }
ExportDetail:
allOf:
- $ref: '#/components/schemas/Export'
- type: object
properties:
files:
type: array
items:
type: object
properties:
id: { type: string, format: uuid }
thumbnail: { type: string }
path: { type: string }
size_bytes: { type: integer, format: int64 }
file_type: { type: string }
RenderNode:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
region: { type: string }
node_ip: { type: string }
worker_port: { type: integer }
status: { type: string }
node_kind: { type: string }
owner_user_id: { type: string, format: uuid, nullable: true }
current_ae_version: { type: string }
node_agent_version: { type: string }
ram_gb: { type: integer }
cpu_cores: { type: integer }
priority: { type: integer }
is_active: { type: boolean }
accepts_new_jobs: { type: boolean }
last_heartbeat_at: { type: string, format: date-time }
current_job_id: { type: string, format: uuid, nullable: true }
last_cpu_pct: { type: integer }
last_ram_available_mb: { type: integer }
ae_running: { type: boolean }
cache_used_gb: { type: integer }
cached_template_count: { type: integer }
lifetime_task_count: { type: integer, format: int64 }
lifetime_crash_count: { type: integer }
consecutive_failures: { type: integer }
RenderNodeCreate:
type: object
required: [name, region, node_ip, worker_port, current_ae_version]
properties:
name: { type: string }
region: { type: string }
node_ip: { type: string }
worker_port: { type: integer }
current_ae_version: { type: string }
ram_gb: { type: integer }
cpu_cores: { type: integer }
node_kind: { type: string }
owner_user_id: { type: string, format: uuid }
priority: { type: integer }
RenderNodeDetail:
allOf:
- $ref: '#/components/schemas/RenderNode'
- type: object
properties:
available_ae_versions: { type: array, items: { type: string } }
cached_template_md5s: { type: array, items: { type: string } }
last_maintenance_at: { type: string, format: date-time }
next_maintenance_at: { type: string, format: date-time, nullable: true }
NodeHealth:
type: object
properties:
node_id: { type: string, format: uuid }
recorded_at: { type: string, format: date-time }
status: { type: string }
cpu_pct: { type: integer }
ram_available_mb: { type: integer }
ae_running: { type: boolean }
current_job_id: { type: string, format: uuid, nullable: true }
current_frame: { type: integer, nullable: true }
cache_used_gb: { type: integer }
NodeHeartbeat:
allOf:
- $ref: '#/components/schemas/NodeHealth'
NodeCrash:
type: object
properties:
id: { type: string, format: uuid }
node_id: { type: string, format: uuid }
render_job_id: { type: string, format: uuid, nullable: true }
crashed_at: { type: string, format: date-time }
last_known_frame: { type: integer, nullable: true }
crash_signal: { type: string }
error_log: { type: string }
log_file_url: { type: string }
auto_recovered: { type: boolean }
recovery_action: { type: string }
recovered_at: { type: string, format: date-time, nullable: true }
NodeUpdate:
type: object
properties:
id: { type: string, format: uuid }
update_file_name: { type: string }
update_number: { type: integer }
description: { type: string }
target_ae_version: { type: string }
in_update_queue: { type: boolean }
rolled_out_to_node_ids: { type: array, items: { type: string, format: uuid } }
create_date: { type: string, format: date-time }
+661
View File
@@ -0,0 +1,661 @@
openapi: 3.0.3
info:
title: FlatRender Studio Service (internal)
version: 1.0.0
description: |
User's saved projects (the editor's state). Includes voiceover +
audio mix settings.
servers:
- url: http://studio-svc.internal/v1
security:
- BearerAuth: []
- ServiceToken: []
tags:
- name: SavedProjects
- name: SavedScenes
- name: Audio
- name: Internal
paths:
/saved-projects:
get:
tags: [SavedProjects]
summary: List user's saved projects
parameters:
- { name: q, in: query, schema: { type: string } }
- { name: type, in: query, schema: { type: string, enum: [Draft, Active, Archived, Trash] } }
- { name: page, in: query, schema: { type: integer } }
- { name: page_size, in: query, schema: { type: integer } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/SavedProjectSummary' } }
meta: { $ref: '#/components/schemas/PaginationMeta' }
post:
tags: [SavedProjects]
summary: Create new saved project from a template
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [original_project_id]
properties:
original_project_id: { type: string, format: uuid }
name: { type: string }
preset_story_id: { type: string, format: uuid }
copy_default_values: { type: boolean, default: true }
responses:
'201':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProject' }
/saved-projects/{id}:
get:
tags: [SavedProjects]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProjectFull' }
patch:
tags: [SavedProjects]
summary: Update top-level fields (name, audio, etc.)
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProjectUpdate' }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProject' }
delete:
tags: [SavedProjects]
summary: Soft-delete (move to trash)
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'204': { description: Trashed }
/saved-projects/{id}/restore:
post:
tags: [SavedProjects]
summary: Restore from trash
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProject' }
/saved-projects/{id}/duplicate:
post:
tags: [SavedProjects]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
content:
application/json:
schema:
type: object
properties:
new_name: { type: string }
responses:
'201':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProject' }
/saved-projects/{id}/autosave:
put:
tags: [SavedProjects]
summary: Autosave entire project graph (debounced from UI)
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProjectFull' }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
saved_at: { type: string, format: date-time }
version: { type: integer }
# ===================== AUDIO (NEW) =====================
/saved-projects/{id}/audio:
get:
tags: [Audio]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/AudioSettings' }
put:
tags: [Audio]
summary: Update audio mix
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AudioSettings' }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/AudioSettings' }
/saved-projects/{id}/voiceover:
post:
tags: [Audio]
summary: |
Upload or record voiceover. Returns target file_id.
Use file service upload endpoints for actual binary; this
attaches an existing file to the project.
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [file_id]
properties:
file_id: { type: string, format: uuid, description: existing user_file_id }
recorded_in_browser: { type: boolean, default: false }
volume: { type: number, minimum: 0, maximum: 1, default: 1.0 }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/AudioSettings' }
delete:
tags: [Audio]
summary: Remove voiceover
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'204': { description: Removed }
/saved-projects/{id}/music:
put:
tags: [Audio]
summary: Set music track (from library or upload)
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
music_track_id: { type: string, format: uuid }
music_file_id: { type: string, format: uuid }
volume: { type: number, minimum: 0, maximum: 1 }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/AudioSettings' }
# ===================== SCENES =====================
/saved-projects/{id}/scenes:
get:
tags: [SavedScenes]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/SavedSceneFull' } }
post:
tags: [SavedScenes]
summary: Add a scene from project template
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [original_scene_id]
properties:
original_scene_id: { type: string, format: uuid }
sort: { type: integer }
scene_length_sec: { type: number }
responses:
'201':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedSceneFull' }
/saved-scenes/{scene_id}:
patch:
tags: [SavedScenes]
parameters:
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
scene_length_sec: { type: number }
manual_color_selection: { type: boolean }
selected_color_preset_id: { type: integer, format: int64 }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedScene' }
delete:
tags: [SavedScenes]
parameters:
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
responses:
'204': { description: Removed }
/saved-projects/{id}/scenes/reorder:
post:
tags: [SavedScenes]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [ordered_ids]
properties:
ordered_ids:
type: array
items: { type: integer, format: int64 }
responses:
'204': { description: Reordered }
/saved-scenes/{scene_id}/contents:
put:
tags: [SavedScenes]
summary: Bulk-update contents for a scene
parameters:
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
contents:
type: array
items: { $ref: '#/components/schemas/SavedSceneContent' }
responses:
'200': { description: Updated }
/saved-scenes/{scene_id}/colors:
put:
tags: [SavedScenes]
summary: Bulk-update colors for a scene
parameters:
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
colors:
type: array
items: { $ref: '#/components/schemas/SavedSceneColor' }
responses:
'200': { description: Updated }
/saved-projects/{id}/shared-colors:
put:
tags: [SavedProjects]
summary: Bulk update project-level shared colors
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
colors:
type: array
items: { $ref: '#/components/schemas/SavedSharedColor' }
responses:
'200': { description: Updated }
/saved-projects/{id}/shared-layers:
put:
tags: [SavedProjects]
summary: Bulk update project-level shared layers
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
layers:
type: array
items: { $ref: '#/components/schemas/SavedSharedLayer' }
responses:
'200': { description: Updated }
# ===================== INTERNAL =====================
/internal/saved-projects/{id}/snapshot-for-render:
get:
tags: [Internal]
summary: |
Called by Render Orchestrator to get the full JSX-ready payload.
Returns everything needed to generate JSX (FIX/FLEXIBLE/Mockup/MV).
security: [ServiceToken: []]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/SavedProjectSnapshot' }
components:
securitySchemes:
BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
ServiceToken: { type: http, scheme: bearer }
schemas:
PaginationMeta:
type: object
properties:
page: { type: integer }
page_size: { type: integer }
total: { type: integer }
has_more: { type: boolean }
SavedProjectSummary:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
image: { type: string }
type: { type: string }
original_project_id: { type: string, format: uuid }
original_project_name: { type: string }
original_container_slug: { type: string }
choose_mode: { type: string }
resolution: { type: string }
project_duration_sec: { type: number }
scene_count: { type: integer }
last_edit_date: { type: string, format: date-time }
created_at: { type: string, format: date-time }
SavedProject:
allOf:
- $ref: '#/components/schemas/SavedProjectSummary'
- type: object
properties:
frame_rate: { type: integer }
vip_factor: { type: number }
manual_color_picker: { type: boolean }
selected_preset_story_id: { type: string, format: uuid, nullable: true }
audio: { $ref: '#/components/schemas/AudioSettings' }
SavedProjectUpdate:
type: object
properties:
name: { type: string }
type: { type: string, enum: [Draft, Active, Archived, Trash] }
manual_color_picker: { type: boolean }
selected_preset_story_id: { type: string, format: uuid, nullable: true }
last_edit_step: { type: string }
SavedProjectFull:
allOf:
- $ref: '#/components/schemas/SavedProject'
- type: object
properties:
scenes: { type: array, items: { $ref: '#/components/schemas/SavedSceneFull' } }
shared_colors: { type: array, items: { $ref: '#/components/schemas/SavedSharedColor' } }
shared_color_presets: { type: array, items: { $ref: '#/components/schemas/SavedSharedColorPreset' } }
shared_layers: { type: array, items: { $ref: '#/components/schemas/SavedSharedLayer' } }
AudioSettings:
type: object
properties:
music_track_id: { type: string, format: uuid, nullable: true }
music_file_id: { type: string, format: uuid, nullable: true }
music_url: { type: string, nullable: true }
music_duration_sec: { type: number, nullable: true }
music_volume: { type: number, minimum: 0, maximum: 1 }
voiceover_file_id: { type: string, format: uuid, nullable: true }
voiceover_url: { type: string, nullable: true }
voiceover_duration_sec: { type: number, nullable: true }
voiceover_volume: { type: number, minimum: 0, maximum: 1 }
voiceover_recorded_in_browser: { type: boolean }
sfx_enabled: { type: boolean }
sfx_volume: { type: number, minimum: 0, maximum: 1 }
SavedScene:
type: object
properties:
id: { type: integer, format: int64 }
saved_project_id: { type: string, format: uuid }
original_scene_id: { type: string, format: uuid }
key: { type: string }
title: { type: string }
image: { type: string }
demo: { type: string }
scene_type: { type: string }
sort: { type: integer }
scene_length_sec: { type: number }
min_duration_sec: { type: number }
max_duration_sec: { type: number }
overlap_at_end_sec: { type: number }
manual_color_selection: { type: boolean }
SavedSceneFull:
allOf:
- $ref: '#/components/schemas/SavedScene'
- type: object
properties:
contents: { type: array, items: { $ref: '#/components/schemas/SavedSceneContent' } }
colors: { type: array, items: { $ref: '#/components/schemas/SavedSceneColor' } }
color_presets: { type: array, items: { $ref: '#/components/schemas/SavedSceneColorPreset' } }
characters: { type: array, items: { $ref: '#/components/schemas/SavedSceneCharacter' } }
SavedSceneContent:
type: object
properties:
id: { type: integer, format: int64 }
key: { type: string }
type: { type: string }
value: { type: string }
value_file_id: { type: string, format: uuid, nullable: true }
file_url_cached: { type: string, nullable: true }
inserted_file_type: { type: string, nullable: true }
font_face: { type: string, nullable: true }
font_size: { type: integer, nullable: true }
justify: { type: string }
position_in_container: { type: integer }
direction_layer_value: { type: integer }
is_text_box: { type: boolean }
ai_input_type: { type: string, nullable: true }
selected_dp: { type: integer, nullable: true }
repeater_item_key: { type: string, nullable: true }
repeater_index: { type: integer, nullable: true }
sort: { type: integer }
SavedSceneColor:
type: object
properties:
id: { type: integer, format: int64 }
element_key: { type: string }
title: { type: string }
icon: { type: string }
attr_value: { type: string }
value: { type: string }
is_selected: { type: boolean }
sort: { type: integer }
SavedSceneColorPreset:
type: object
properties:
id: { type: integer, format: int64 }
is_selected: { type: boolean }
sort: { type: integer }
items:
type: array
items:
type: object
properties:
element_key: { type: string }
value: { type: string }
sort: { type: integer }
SavedSceneCharacter:
type: object
properties:
id: { type: integer, format: int64 }
key: { type: string, format: uuid }
name: { type: string }
icon: { type: string }
controllers:
type: array
items:
type: object
properties:
name: { type: string }
key: { type: string }
value: { type: string }
sort: { type: integer }
SavedSharedColor:
type: object
properties:
id: { type: integer, format: int64 }
element_key: { type: string }
title: { type: string }
attr_value: { type: string }
value: { type: string }
is_selected: { type: boolean }
sort: { type: integer }
SavedSharedColorPreset:
type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
is_selected: { type: boolean }
sort: { type: integer }
items:
type: array
items:
type: object
properties:
element_key: { type: string }
value: { type: string }
SavedSharedLayer:
type: object
properties:
id: { type: integer, format: int64 }
key: { type: string }
title: { type: string }
type: { type: string }
value: { type: string }
value_file_id: { type: string, format: uuid, nullable: true }
file_url_cached: { type: string, nullable: true }
font_face: { type: string }
font_size: { type: integer }
justify: { type: string }
position_in_container: { type: integer }
direction_layer_value: { type: integer }
is_text_box: { type: boolean }
sort: { type: integer }
SavedProjectSnapshot:
type: object
description: |
Complete payload for JSX generation. Same shape returned for any
choose_mode; render service decides which JSX generator to use.
properties:
saved_project_id: { type: string, format: uuid }
tenant_id: { type: string, format: uuid }
user_id: { type: string, format: uuid }
original_project_id: { type: string, format: uuid }
original_project_name: { type: string }
choose_mode: { type: string }
resolution: { type: string }
frame_rate: { type: integer }
project_duration_sec: { type: number }
vip_factor: { type: number }
aep:
type: object
properties:
url: { type: string }
md5: { type: string }
size_bytes: { type: integer, format: int64 }
render_comp: { type: string }
original_width: { type: integer }
original_height: { type: integer }
audio: { $ref: '#/components/schemas/AudioSettings' }
shared_colors:
type: array
items: { $ref: '#/components/schemas/SavedSharedColor' }
shared_layers:
type: array
items: { $ref: '#/components/schemas/SavedSharedLayer' }
scenes:
type: array
items: { $ref: '#/components/schemas/SavedSceneFull' }