Files
soroush.asadi 90ac0b81d1 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>
2026-05-29 23:29:31 +03:30

824 lines
27 KiB
YAML

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 }