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:
@@ -0,0 +1,219 @@
|
||||
# =====================================================================
|
||||
# Render Events — published by Render Orchestrator
|
||||
# Routing: flatrender.events (topic) — key: render.*.v1
|
||||
# =====================================================================
|
||||
$schema: "http://json-schema.org/draft-07/schema#"
|
||||
title: Render Event Schemas
|
||||
type: object
|
||||
|
||||
definitions:
|
||||
EventEnvelope:
|
||||
type: object
|
||||
required: [event_id, event_type, event_time, tenant_id, data]
|
||||
properties:
|
||||
event_id: { type: string, format: uuid }
|
||||
event_type: { type: string }
|
||||
event_time: { type: string, format: date-time }
|
||||
tenant_id: { type: string, format: uuid }
|
||||
user_id: { type: string, format: uuid }
|
||||
trace_id: { type: string, format: uuid }
|
||||
correlation_id: { type: string, format: uuid }
|
||||
producer: { type: string, const: render-orchestrator }
|
||||
data: { type: object }
|
||||
|
||||
events:
|
||||
# -------------------------------------------------------------------
|
||||
# render.job.queued.v1
|
||||
# Emitted when a job is accepted into the queue.
|
||||
# Consumed by: notification (none yet), analytics
|
||||
# -------------------------------------------------------------------
|
||||
render.job.queued.v1:
|
||||
routing_key: render.job.queued.v1
|
||||
description: New render job has been accepted into a priority queue.
|
||||
payload:
|
||||
type: object
|
||||
required: [render_job_id, saved_project_id, priority_queue, price_type, region]
|
||||
properties:
|
||||
render_job_id: { type: string, format: uuid }
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
original_project_id: { type: string, format: uuid }
|
||||
priority_queue:
|
||||
type: string
|
||||
enum: [snapshot, vip, paid, preview, mockup, voiceover]
|
||||
priority_score: { type: integer, minimum: 0, maximum: 100 }
|
||||
price_type:
|
||||
type: string
|
||||
enum: [Free, Preview, Cash, Plan, Snapshot, Reseller]
|
||||
paid_price_minor: { type: integer, format: int64 }
|
||||
region: { type: string }
|
||||
duration_sec: { type: number }
|
||||
resolution: { type: string }
|
||||
is_60_fps: { type: boolean }
|
||||
has_music: { type: boolean }
|
||||
has_sfx: { type: boolean }
|
||||
has_voiceover: { type: boolean }
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.job.started.v1
|
||||
# Emitted once nodes are assigned and rendering begins.
|
||||
# -------------------------------------------------------------------
|
||||
render.job.started.v1:
|
||||
routing_key: render.job.started.v1
|
||||
description: Render job has begun processing on at least one node.
|
||||
payload:
|
||||
type: object
|
||||
required: [render_job_id, started_at]
|
||||
properties:
|
||||
render_job_id: { type: string, format: uuid }
|
||||
started_at: { type: string, format: date-time }
|
||||
node_ids:
|
||||
type: array
|
||||
items: { type: string, format: uuid }
|
||||
region: { type: string }
|
||||
total_frames: { type: integer }
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.job.progress.v1 (high-frequency, optional pub/sub)
|
||||
# NOT broadcast on the main events exchange — only on per-job
|
||||
# ephemeral fanout for WebSocket fan-out. Schema documented here.
|
||||
# -------------------------------------------------------------------
|
||||
render.job.progress.v1:
|
||||
routing_key: render.job.progress.{job_id}
|
||||
description: Progress tick for live UI update.
|
||||
exchange: render.progress # separate dedicated exchange (auto-delete fanout)
|
||||
payload:
|
||||
type: object
|
||||
required: [render_job_id, step, progress]
|
||||
properties:
|
||||
render_job_id: { type: string, format: uuid }
|
||||
step:
|
||||
type: string
|
||||
enum: [Queued, Preparing, TemplateCache, JsxGen, Music,
|
||||
Rendering, Validating, Repairing, Optimisation, Video,
|
||||
Mixing, Final, Uploading, Done, Failed, Cancelled]
|
||||
progress: { type: integer, minimum: 0, maximum: 100 }
|
||||
current_frame: { type: integer, nullable: true }
|
||||
total_frames: { type: integer, nullable: true }
|
||||
eta_seconds: { type: integer, nullable: true }
|
||||
preview_b64:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Last rendered frame thumbnail (small, ~5-15 KB)
|
||||
message: { type: string, nullable: true }
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.job.completed.v1
|
||||
# Consumed by: notification, studio (mark project), file (cleanup),
|
||||
# tenant (usage metering), webhook dispatcher
|
||||
# -------------------------------------------------------------------
|
||||
render.job.completed.v1:
|
||||
routing_key: render.job.completed.v1
|
||||
description: Render job successfully produced an export.
|
||||
payload:
|
||||
type: object
|
||||
required: [render_job_id, export_id, output_url, duration_sec, size_bytes]
|
||||
properties:
|
||||
render_job_id: { type: string, format: uuid }
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
original_project_id: { type: string, format: uuid }
|
||||
export_id: { type: string, format: uuid }
|
||||
output_url: { type: string }
|
||||
thumbnail_url: { type: string, nullable: true }
|
||||
duration_sec: { type: number }
|
||||
size_bytes: { type: integer, format: int64 }
|
||||
resolution: { type: string }
|
||||
width: { type: integer }
|
||||
height: { type: integer }
|
||||
render_compute_seconds: { type: integer, description: "Sum of node-seconds spent" }
|
||||
node_ids_used:
|
||||
type: array
|
||||
items: { type: string, format: uuid }
|
||||
price_type: { type: string }
|
||||
paid_price_minor: { type: integer, format: int64 }
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.job.failed.v1
|
||||
# Consumed by: notification, identity (refund), tenant (metering)
|
||||
# -------------------------------------------------------------------
|
||||
render.job.failed.v1:
|
||||
routing_key: render.job.failed.v1
|
||||
description: Render job failed permanently (after retries exhausted).
|
||||
payload:
|
||||
type: object
|
||||
required: [render_job_id, failed_at_step, error_message]
|
||||
properties:
|
||||
render_job_id: { type: string, format: uuid }
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
failed_at_step:
|
||||
type: string
|
||||
enum: [Queued, Preparing, TemplateCache, JsxGen, Music,
|
||||
Rendering, Validating, Repairing, Optimisation, Video,
|
||||
Mixing, Final, Uploading, Done, Failed, Cancelled]
|
||||
error_message: { type: string }
|
||||
error_code: { type: string, nullable: true }
|
||||
retry_count: { type: integer }
|
||||
refund_required: { type: boolean, default: true }
|
||||
price_type: { type: string }
|
||||
paid_price_minor: { type: integer, format: int64 }
|
||||
node_ids_attempted:
|
||||
type: array
|
||||
items: { type: string, format: uuid }
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.job.cancelled.v1
|
||||
# User cancelled via UI before completion.
|
||||
# -------------------------------------------------------------------
|
||||
render.job.cancelled.v1:
|
||||
routing_key: render.job.cancelled.v1
|
||||
description: Render job was cancelled by user.
|
||||
payload:
|
||||
type: object
|
||||
required: [render_job_id, cancelled_by_user_id]
|
||||
properties:
|
||||
render_job_id: { type: string, format: uuid }
|
||||
cancelled_by_user_id: { type: string, format: uuid }
|
||||
cancelled_at_step: { type: string }
|
||||
progress_when_cancelled: { type: integer }
|
||||
refund_required: { type: boolean, default: true }
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.snapshot.requested.v1
|
||||
# User asked for a single-frame snapshot of a scene.
|
||||
# -------------------------------------------------------------------
|
||||
render.snapshot.requested.v1:
|
||||
routing_key: render.snapshot.requested.v1
|
||||
description: Scene snapshot was queued.
|
||||
payload:
|
||||
type: object
|
||||
required: [snapshot_id, saved_project_id, scene_key, frame_number]
|
||||
properties:
|
||||
snapshot_id: { type: string, format: uuid }
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
scene_key: { type: string }
|
||||
frame_number: { type: integer }
|
||||
cached:
|
||||
type: boolean
|
||||
description: True if served from cache (no render needed)
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render.snapshot.ready.v1
|
||||
# Single-frame snapshot finished.
|
||||
# -------------------------------------------------------------------
|
||||
render.snapshot.ready.v1:
|
||||
routing_key: render.snapshot.ready.v1
|
||||
description: Snapshot image is ready.
|
||||
payload:
|
||||
type: object
|
||||
required: [snapshot_id, image_url]
|
||||
properties:
|
||||
snapshot_id: { type: string, format: uuid }
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
scene_key: { type: string }
|
||||
frame_number: { type: integer }
|
||||
image_url: { type: string }
|
||||
thumbnail_url: { type: string, nullable: true }
|
||||
width: { type: integer }
|
||||
height: { type: integer }
|
||||
size_bytes: { type: integer }
|
||||
duration_ms: { type: integer }
|
||||
expires_at: { type: string, format: date-time }
|
||||
Reference in New Issue
Block a user