Skip to content

How it works

fxquinox is metadata-driven: the things you track (projects, shots, assets, tasks, versions…) are not hard-coded tables — they are defined as data by a schema engine and stored as a single, uniform record type. One core enforces the rules and exposes them through one API; every tool is a client of that API.

The mental model in one line

Schema describes what kinds of things exist and how they link; records are the actual things; the publish chain records what produced what; the API is the only door in.

The big picture

flowchart LR
  PG[("Postgres<br/>records · JSONB")]:::data --> CORE

  subgraph CORE["Core · FastAPI"]
    direction TB
    SE("Schema engine"):::core
    QL("Query DSL"):::core
    PUB("Publish chain<br/>+ lineage"):::core
    AU("Auth"):::core
    EV("Events · WebSocket"):::core
    ME("Media"):::core
  end

  CORE --> API{{"Canonical HTTP API"}}:::api

  API --> SDK(["Python SDK"]):::client
  API --> WEB(["Web UI"]):::client
  API --> QT(["Qt desktop"]):::client
  API --> DCC(["Houdini · Nuke"]):::client

  style CORE fill:#0a0f20,stroke:#1c2438,color:#9fb0c8

  classDef data fill:#0c2a23,stroke:#1cbc90,color:#bff3e2,stroke-width:1.5px;
  classDef core fill:#0f1a3c,stroke:#3a5bf0,color:#dbe4ff,stroke-width:1.5px;
  classDef api fill:#002fa7,stroke:#8aa0f8,color:#f4f6ff,stroke-width:1.5px;
  classDef client fill:#221a3c,stroke:#a06bff,color:#ece2ff,stroke-width:1.5px;

Everything above the API is a client. None of them touch the database or re-implement workflow rules — that is The One Law, and it is what keeps four very different surfaces behaving identically.

Two layers: schema and data

There are really only two kinds of thing in the system.

The schema layer (the configuration) defines the object types, their fields, the available statuses, the pipeline steps and priorities, and which transitions are legal. The data layer is a single Record table: every project, shot, task and version is a row whose shape is governed by its entity type, with all its fields living in a JSON data column and relationships expressed as links.

flowchart LR
  subgraph SCHEMA["Schema layer · configuration"]
    direction TB
    ET("EntityType<br/><i>code · hierarchy</i>"):::core
    FD("FieldDef<br/><i>data_type · config</i>"):::core
    ST("Status<br/><i>state · flags</i>"):::core
  end

  REC[("Record<br/><i>entity_type · status · data (JSON)</i>")]:::data

  ET -->|"fields"| FD
  ET -->|"statuses"| ST
  REC -.->|"typed by"| ET

  classDef core fill:#0f1a3c,stroke:#3a5bf0,color:#dbe4ff,stroke-width:1.5px;
  classDef data fill:#0c2a23,stroke:#1cbc90,color:#bff3e2,stroke-width:1.5px;
  style SCHEMA fill:#0a0f20,stroke:#1c2438,color:#9fb0c8

Because types are data, you can add a custom entity type or field at runtime and every client picks it up — the grid, the SDK, and the API all derive their shape from the schema rather than from compiled code.

Why one Record table instead of one table per type?

A uniform record + JSON means new types and fields need no migrations, the query DSL can filter across any type, and the web grid, board and sidebar are all driven by the same schema metadata. Links between records are stored as { "type": "Shot", "id": "…" } field values, so the relationship graph is just data too.

The production hierarchy

Production entities nest into a tree. Episodes are optional (used for episodic work); shots live under sequences; assets sit directly under the project. A Task is the unit of work and hangs off whatever it tracks.

graph TD
  P(["Project"]):::ctx --> EP(["Episode<br/><i>optional</i>"]):::ctx
  P --> SEQ(["Sequence"]):::ctx
  EP --> SEQ
  SEQ --> SH(["Shot"]):::ctx
  P --> AS(["Asset"]):::ctx

  SH --> T("Task"):::work
  AS --> T
  SEQ -.-> T
  EP -.-> T

  classDef ctx fill:#0e1f3c,stroke:#4493f8,color:#d6e9ff,stroke-width:1.5px;
  classDef work fill:#2c1d0c,stroke:#e6932b,color:#ffe6c2,stroke-width:1.5px;

A task carries a pipeline step (Layout, Animation, FX, Lighting, Compositing…), assignees, a priority, bid hours and dates.

The project workspace showing the hierarchy and task Gantt
The same model in the app: the project → sequence → shot → task hierarchy, with each task's step, status, assignee and schedule.

How entities relate

Production context, the work, and the outputs it produces — colour-grouped by domain.

flowchart LR
  subgraph PROD["Production"]
    direction TB
    PRJ(["Project"]):::ctx --> EPI(["Episode"]):::ctx
    EPI --> SEQ(["Sequence"]):::ctx
    SEQ --> SHO(["Shot"]):::ctx
    PRJ --> ASS(["Asset"]):::ctx
  end

  subgraph WORK["Work"]
    direction TB
    TASK("Task"):::work
    TL("TimeLog"):::work
  end

  subgraph PUB["Publish & files"]
    direction TB
    PRD(["Product"]):::out --> VER(["Version"]):::out
    VER --> REP(["Representation"]):::out
    REP --> FLOC(["FileLocation"]):::out
    WF[/"Workfile"/]:::muted
    STE[("Site")]:::data
  end

  USR(["User"]):::people

  SHO ==> TASK
  ASS ==> TASK
  USR -. assignee .-> TASK
  TASK ==> PRD
  WF -. source .-> VER
  VER -. inputs .-> VER
  STE --> FLOC
  TASK --- TL

  classDef ctx fill:#0e1f3c,stroke:#4493f8,color:#d6e9ff,stroke-width:1.5px;
  classDef work fill:#2c1d0c,stroke:#e6932b,color:#ffe6c2,stroke-width:1.5px;
  classDef out fill:#0c2a1b,stroke:#1cbc90,color:#c7f3dd,stroke-width:1.5px;
  classDef people fill:#2c250c,stroke:#f1c40f,color:#fbeec2,stroke-width:1.5px;
  classDef data fill:#0c2a23,stroke:#1cbc90,color:#bff3e2,stroke-width:1.5px;
  classDef muted fill:#1a2030,stroke:#6b7488,color:#cdd3e0,stroke-width:1.5px;
  style PROD fill:#0a1124,stroke:#1c2438,color:#9fb0c8
  style WORK fill:#1a1408,stroke:#3a2c12,color:#c9a574
  style PUB fill:#08180f,stroke:#143226,color:#7fb89e

The entity catalog

What each built-in type is. (Types are data — projects can add their own.)

Project
Top of the tree. Holds show-wide settings (code, fps, dates) and scopes everything beneath it.
Episode
Optional grouping for episodic work; parents sequences.
Sequence
A group of shots; parented by an episode (or directly by the project).
Shot
A single continuous piece of footage — cut range, frame handles, fps, priority.
Asset
A reusable element (Character, Creature, Environment, Prop, Vehicle, and more); the asset_type is a configurable, workflow-scoped enum.
Task
The unit of work. Links to the entity it tracks, carries a pipeline step, assignees, priority, bid hours, and dates.
TimeLog
Hours booked against a task by a user on a given day.
Product
A named output of a task (e.g. lightingMain) — the stable identity a chain of versions shares.
Version
A numbered iteration of a product. Records its author, a comment, the source workfile and its input versions (lineage). Carries frame range and fps metadata.
Representation
A concrete form of a version (e.g. exr, mov), flagged for review or not, with files and tags.
Workfile
The DCC scene a version was published from (path, software, version number).
Site · FileLocation
A storage location, and where a representation actually lives on it (path, availability) — the basis for multi-site sync.
Note
Feedback attached to a shot, asset, task or version. Supports @mentions, labels, and threaded replies.
Playlist
An ordered set of versions for review sessions.
User
A person (name, email, role). Assigned to tasks; authors versions.
ApiKey · SavedView · Automation · Webhook
System types: programmatic access tokens, saved filters/layouts, rule-driven automations, and outbound webhooks.

The publish chain & lineage

Publishing is the backbone that links the work to the files it produced, and links versions to the versions they were built from.

flowchart TD
  T("Task"):::work ==> PR(["Product<br/><i>named output</i>"]):::out
  PR ==> V(["Version<br/><i>v001, v002…</i>"]):::out
  WF[/"Workfile"/]:::muted -. source .-> V
  V -. inputs .-> V
  V ==> REP(["Representation<br/><i>exr · mov</i>"]):::out
  REP ==> FL(["FileLocation"]):::out
  STE[("Site")]:::data --> FL

  classDef work fill:#2c1d0c,stroke:#e6932b,color:#ffe6c2,stroke-width:1.5px;
  classDef out fill:#0c2a1b,stroke:#1cbc90,color:#c7f3dd,stroke-width:1.5px;
  classDef data fill:#0c2a23,stroke:#1cbc90,color:#bff3e2,stroke-width:1.5px;
  classDef muted fill:#1a2030,stroke:#6b7488,color:#cdd3e0,stroke-width:1.5px;

Publishing a task creates (or reuses) a Product, allocates the next Version number, attaches one or more Representations, and records a FileLocation per site. A version's inputs point at the versions it consumed — so the chain forms a lineage DAG you can walk in either direction ("what did this come from?" / "what depends on this?").

Workflow & statuses

Each entity type has its own set of statuses; a WorkflowSchema assigned to a project scopes which statuses (and steps and asset types) are available. A typical task lifecycle — colours match the status palette used across the app:

stateDiagram-v2
  direction LR
  [*] --> wtg
  wtg --> ip : start
  ip --> rev : submit
  rev --> fin : approve
  rev --> ip : retake
  ip --> hld : block
  hld --> ip : resume
  wtg --> omt : omit
  ip --> omt : omit
  fin --> [*]

  classDef waiting fill:#1a2030,stroke:#8089a0,color:#dfe4ee,stroke-width:1.5px
  classDef wip fill:#0f1a3c,stroke:#3a5bf0,color:#dbe4ff,stroke-width:1.5px
  classDef review fill:#2c1d0c,stroke:#e6932b,color:#ffe6c2,stroke-width:1.5px
  classDef done fill:#0c2a1b,stroke:#1cbc90,color:#c7f3dd,stroke-width:1.5px
  classDef hold fill:#2c0f12,stroke:#e74c3c,color:#ffd6dd,stroke-width:1.5px
  classDef cancelled fill:#1a2030,stroke:#607d8b,color:#cfd8dc,stroke-width:1.5px

  class wtg waiting
  class ip wip
  class rev review
  class fin done
  class hld hold
  class omt cancelled

Statuses carry flags the whole app reads — so behaviour is driven by data, not hard-coded names:

Code Display name State Flags
waitingToStart Waiting to Start not_started
inProgress In Progress in_progress is_wip
pendingReview Pending Review in_progress is_feedback
final Final done is_done
onHold On Hold blocked
omitted Omitted cancelled is_done

Projects carry their own statuses (active, onHold, archived). Legal transitions are themselves data (Workflow rows with optional role gates), so the same enforcement applies no matter which client requests the change.