
Building Production Desk: The Architecture of a Local Film Workflow
A local production system for story, boards, generations, assets, review, and editorial timing, built around explicit boundaries.
Production Desk is designed as a local working environment for film production. This entry follows the engineering boundary behind the screens: the Electron shell, Next.js server, project store, jobs, media, and timeline.
The screen order and production workflow are documented in the companion product journal. This entry stays with the implementation decisions that support that movement.
The full engineering source is available in the Production Desk architecture.md file.
A / SCREEN OWNERSHIP
A frontend is also a map of ownership
Production Desk screens are not a collection of toolbars. The Electron main process owns the native window, project catalog, preferences, folder dialogs, and child-process lifecycle. The Next.js server owns the local API for production data, jobs, media, and review. The React renderer translates those boundaries into usable actions.
That division shapes the screen composition. Home owns the catalog and create or open actions. The project shell owns the tree, current context, tabs, and persistence. Boards gives the canvas the largest surface; Timeline gives frame editing the largest surface; Generations gives prompts, references, preflight, and candidate decisions the clearest path. A shared shell can still give each task its own density.
Navigation also carries unfinished context. Creation mode, selected node, expanded tree state, sidebar state, tab, and unsaved draft are retained, while project switching and quitting wait for a renderer response. This is an interaction contract for preserving working context, not a visual flourish.
Start by fixing the boundaries
The Electron main process owns the native window, catalog, preferences, folder dialogs, project switching, and the lifecycle of the server it starts. Preload exposes a narrow window.productionDesk bridge. The renderer receives no absolute paths, credential values, or shell commands.
At launch, the shell chooses a free loopback port, starts the standalone Next.js server, and trusts only that active origin. Before create, switch, close, or quit, it waits for a renderer response and denies the operation after ten seconds. Native actions and web state therefore have one guarded handoff.
The same choice gives failure a clear meaning. If the server cannot start, the shell does not continue into the new root and rolls catalog and configuration back. If the renderer does not answer within ten seconds, the native action is denied. A visible UI and a safe native state transition are related, but they are not the same event.
Keep production state in one graph
ProjectState is a manifest containing a schema version, stable project ID, revision, nodes, assets, boards, timelines, and worlds. Nodes form the Act, Scene, Sequence, and Shot hierarchy. Boards and worlds stay scoped to nodes.
The graph also defines the frontend's selection scope. Library is a picker filtered by the current node and asset type, rather than a permanent gallery. The asset catalog selects versions; a Board keeps shot identity and page order; a Timeline clip references an asset, a version, or a child timeline. The object selected on screen should correspond to the relationship that will be persisted.
Each update includes an expected revision. A mismatch returns the current project as a conflict instead of applying last-write-wins. Media bytes use SHA-256 content addressing, so new bytes create a new version and existing lineage remains inspectable.
D / VISIBLE CONFLICTS
Saving must leave a conflict visible
An update to the project store includes an expected revision. When that revision is stale, the API returns a conflict and the latest ProjectState. Imagine two editors reading revision 18: one saves a screenplay, then the other tries to overwrite it with an older board state. Production Desk does not silently choose last-write-wins. It leaves reconciliation as a visible decision.
Mutable JSON is written through a temporary file, fsync, and rename. Media bytes are addressed by SHA-256. A file referenced by a Board, clip, selected version, child timeline, or world output cannot be treated as an unreferenced delete. The frontend therefore needs states that explain referenced material, conflicts, and recovery receipts instead of pretending every delete is immediate.
E / DISCOVERY TO APPLY
Existing production folders are observed first
Source discovery does not follow symlinks. It excludes credentials, caches, and unapproved paths, then records candidate hashes, byte counts, warnings, and rejected paths in a preview. The 5RPS and TEGAKI profiles have different source roles, so the same folder is not forced into one shape. A custom profile is an explicit mapping of contained relative paths.
Migration copies accepted files into the managed blob boundary only after the user reviews the preview and chooses Apply. Its receipt records counts, hashes, bytes, warnings, rejected paths, and preserved originals. Opening a folder alone does not perform that operation.
Applying a change back to an authored source document is a separate boundary. Immediately before writing, it compares the expected revision and source hash. A conflict retains recovery content and stops the write. The frontend should therefore distinguish a read-only migration preview, migration Apply into the managed project, and Apply back to a source document.
F / GENERATION AND CANDIDATES
Make candidates and keep the decision
Boards, image generation, and video generation share the same preparation, reference checks, and approval context. A visible known character requires the canonical bitmap and an identity receipt. A generated result remains a candidate until evaluation and approval.
The generation screen separates requested facts from observed results. The selected model, style, character reference, and Flare or Sunburst preference remain in the request snapshot. The runner receipt records the observed model, while a separate identity receipt records a known character match. A stale prepared package stops at preflight, and paid submission is a separate explicit action.
A candidate is not a finished result before evaluation and approval. The interface shows job status, cancellation markers, recoverable output, and the one-slot queue, with receipts that support retry after failure. Board debounced autosave and undo history are different states from durable version lineage, so the UI keeps their meanings separate.
G / EDITING TIME
Timeline handles frames and composition together
Timeline validates integer frames, no overlap per layer, child references, and acyclic composition. Nested lanes compose into the parent while drag placement, gap preservation, boundary snapping, ripple collision handling, and undo or reset stay in one editing surface. Playback is immediate inspection; rendering is a separate validated handoff to the local boundary.
FFmpeg is a local prerequisite, not an invitation for the frontend to build arbitrary shell commands. The renderer sends a render request to /api/project/render. The server validates the timeline, clips, versions, and frame range before invoking the local runner with an argument array. This keeps playback light while giving export failures a meaningful boundary.
H / LIFECYCLE AND PACKAGING
The boundary that makes it a local application
From the renderer, folder selection, project switching, close, and quit may look like similar clicks. In the application, the main process serializes those actions, checks the active loopback origin, and waits for a lifecycle response. Preload exposes validated methods and opaque DTOs, while absolute paths, credentials, and shell commands stay outside the renderer.
The standalone Next.js server, bundled helpers, and Electron package have separate resource boundaries. Packaging assembles the standalone output into a darwin-arm64 package, while installation uses a guarded atomic install. A packaged app cannot assume the development repository root, so a release records the archive, checksum, installation result, and provider boundaries that remain unverified.
I / RECEIPTS DEFINE CLOUD COMPLETION
Local completion is not publication
The local manifest is authoritative. iCloud, private GCS, Supabase metadata, and the outbox are optional paths with separate receipts. When the cloud head and local revision diverge, the UI exposes the cloud-head, backup-local, keep-local, and sync states as an explicit decision. Keep-local also requires the observed cloud hash and confirmation. Supabase is metadata and index storage, not a full manifest restore store.
This boundary keeps completion messages honest. Backup failure, copy failure, unavailable providers, and an unperformed public upload remain visible rather than being folded into success. Credentials are read by the native side from a keychain or machine-local environment and do not enter project files, bundles, logs, or commits.
J / IMPLEMENTATION TRADE-OFFS
Separate responsive editing from durable truth
React owns selection, input, drafts, and immediate previews. The local API owns validation, revisions, durable writes, and job state. This division keeps the speed of browser-based screen development while giving the Electron child process, readiness checks, and native handoffs their own contract. Draft changes can remain responsive without writing every keystroke to disk, while a saved state can be shown only after the API confirms it.
ProjectState CAS protects the graph as one revisioned unit rather than pretending every field is independent. Two screens may read revision 18, one may write revision 19, and the other may then submit its older revision 18 even when the edits touch different areas. That rejection should not trigger a blanket retry. The frontend should show the latest snapshot, compare it with the draft, and let the user choose how to reconcile it.
Revision checking and safe JSON writing solve different failures. CAS identifies an old writer, while temporary-file, fsync, and rename writes prevent a half-written manifest after a process failure. SHA-256 identifies identical media bytes, while a stable shot ID can retain lineage across rough, filled, and productionized versions. Board autosave and undo history belong to editing flow; they do not replace the history of adopted versions.
Time becomes easier to reason about when the unit is fixed. At 24 fps, an illustrative range from frame 24 to frame 48 represents one second, so a screen does not have to guess whether a value means frames or seconds. A child timeline that refers back through its parent would make composition circular, so the cycle is detected before saving. Scrubbing and playback are inspection actions; rendering sends only a validated range through the server boundary.
Generation treats a prepared request as an immutable snapshot. If a reference or prompt changes while an older package remains ready, submitting it could attach cost and output to the wrong intent, so preflight stops it as stale. Asynchronous jobs persist status and results, write a cancellation marker, and terminate the process group when possible. A one-slot queue does not maximize throughput, but it keeps local resource use and billing behavior predictable.
This record keeps implementation contracts separate from end-to-end proof. The architecture snapshot is dated 2026-10-05 and documents origin and lifecycle checks, packaging boundaries, and fixture coverage. Keyboard paths, nested editing, provider execution, paid generation, cloud propagation, GCS sync, and restore UI are not reported as complete merely because their source contracts exist. Future implementation and review can begin from those explicit gaps.
The screens and two renders





Production render: a seated figure in a neon nightclub interior.
Production render: a wider view of the neon nightclub scene.