A branching story is a graph, and the media stays on the writer's own disk
A desktop editor where scenes are nodes and choices are edges, so a writer can see the shape of a branching story, and where audio and images never leave the machine they were recorded on.
Who, what, and how long
Media & Publishing
12 weeks
Fixed price
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
The story is a canvas
Scenes are nodes and choices are edges, so the shape of the branching is on screen for the writer to see.
React Flow renders the story as a graph (scenes as nodes, choices as edges), so the shape of the branching is something a writer looks at instead of reconstructing from a document's headings. Layout is persisted, so a story arranged to make sense stays arranged. Dead ends, unreachable scenes and loops become visible as shapes, and those were exactly the structural problems being found late.
- Scenes as nodes, choices as edges, on a persisted layout
- Dead ends and unreachable scenes visible as shapes
- Structure looked at, not reconstructed from headings
Every flag the story sets, and every scene that reads it: trusts_tom as a lens over the canvas, set in one scene and read in three, above the structure drawn as shapes (two endings, a dead end, an unreachable scene, two loops back) and the layout saved in the story file.
Deleting The Pump House while two choices lead to it: the dialog lists both, deletes them with the scene or keeps the scene, and has no option to leave a choice pointing at a scene that is gone.
Dangling choices are impossible
An edge can't point at a scene that doesn't exist, which removes the entire class of error they were finding in the studio.
An edge is a reference to a scene, so it can't point at one that doesn't exist: deleting a scene either takes its incoming choices with it or is refused with them listed. The entire class of error they had been finding in the studio, a choice leading nowhere, simply can't be represented, so there's nothing to validate.
- Edges are references; a dangling choice can't be created
- Deleting a scene handles or refuses its incoming edges
- The studio's most common defect is now unrepresentable
Dialogue is structured
Speaker and line are fields in an ordered list, never prose somebody has to parse at export.
Dialogue is an ordered list of speaker-and-line records, with no naming convention to follow, so the export writes structure instead of parsing it back out of formatting. That also makes a speaker rename a single operation across a whole story, and it means a line can carry its own audio reference without a separate cue sheet.
- Speaker and line as fields in an ordered list
- Export writes structure, with no prose to re-parse
- A speaker rename is one operation across the story
Inside a scene: six ordered speaker-and-line rows, each with its own audio file and one that plays only if a flag is set, the scene's two choices, and a speaker rename that follows twelve lines across six scenes.
A local audio attachment: a line's recording read in place from the studio's drive, the scene's files listed by path, and the story file holding forty-four audio and nine image references with nothing uploaded.
Media never uploads
Audio and images are local file references, which is the reason this is a desktop tool.
Audio and images are local file references. Nothing uploads, which is precisely why this is a desktop tool: the studio's unreleased voice recordings aren't going through anyone's server, and a browser can't hold a stable reference to a file on disk across sessions. Missing files are reported by path at export, never silently omitted.
- Local file references; nothing leaves the machine
- The reason it's desktop, not web
- Missing media reported by path at export, never dropped
Export validates first
Validation before export, with two errors holding the bundle back: a choice with no scene yet and an audio file missing from disk, each naming its scene and schema path, and no file written.
A story that would break in playback produces an error at the desk instead of a file.
Export validates every scene against a Zod schema before a file is written (required fields present, every edge resolvable, every media reference on disk) and fails at the desk with the offending scene named. A story that would break in playback never becomes a file, which moves the discovery of a fault from a studio session to a writer's afternoon.
- Zod validation of every scene before a file is written
- Failures name the offending scene
- Faults surface at the desk, never in a studio session
What we were brought in to do
The studio writes branching audio fiction. We built the tool they write it in: a canvas of scenes and connectors, a dialogue editor inside each scene, local audio and image attachment, and an export step that validates the whole story before it produces anything.
A studio writing branching audio fiction in a word processor, where every choice was a heading reference typed by hand. A story of any size accumulated references pointing at scenes that had been renamed or deleted, and nobody found them until a playthrough hit one. That made finding them a recording session's job, with voice talent in the room.
Full-Stack Engineering
Where the old way broke
They were writing branching stories in a document. Every choice was a heading reference typed by hand, and a story of any size had references pointing at scenes that had been renamed or deleted. Nobody found those until a playthrough hit them, which meant finding them was a recording session's job.
The document format made the structure invisible and the references unchecked. A writer had to hold the branching shape in their head, and a rename in one place silently broke every choice pointing at it. The cost was concentrated in the most expensive hour of the production: a dangling reference discovered in the studio stops a session.
Make the structure visible and make it checked. Scenes are nodes on a canvas, choices are edges between them, and an edge can't point at a scene that doesn't exist. The export validates every scene against a schema before writing a file, so a broken story fails at the desk, long before it reaches the studio.
What we built together
- 01
Modeled scenes as nodes and choices as edges, so the story's shape is the thing on screen
Scenes are nodes and choices are edges on a persisted canvas, so the branching shape is the thing on screen, with nothing to reconstruct from headings.
- 02
Made a dangling connector impossible, not merely detectable
Making the error impossible, not just detectable, was the decision the tool turns on, and it removed the validation rule, the error message and the discipline together.
- 03
Validated every scene against a schema at export, not at load
Each line of dialogue is stored as a speaker-and-line record, which is what lets export emit clean structure.
- 04
Kept audio and images as local files, which is the reason this is a desktop application
Keeping media on disk also settled the desktop question: no browser can hold a durable handle to a file across sessions, embargo or not.
- 05
Ran the export in the main process, so a large story doesn't freeze the canvas
Export validates every scene against a schema and fails at the desk with the offending scene named, so no file is produced that would break in playback.
Phase by phase
Phase 1: The Canvas
Scenes And Connectors
Built the graph canvas with custom scene nodes and choice edges. Position is saved with the story, because a writer's mental map of their own plot is worth persisting.
- Scene Nodes
- Choice Edges
- Canvas State
Phase 2: Inside A Scene
Dialogue As A Repeater
Built the scene editor with dialogue as an ordered repeater (speaker and line, added and reordered) in place of a free-text block that has to be parsed later.
- Scene Editor
- Dialogue Repeater
- Speaker Handling
Phase 3: Media
It Stays On The Disk
Built audio and image attachment against local files. This is why the tool is a desktop application: raw recordings are large, unreleased, and belong on the machine that made them.
- Audio Attachment
- Image Attachment
- Local References
Phase 4: Validation And Export
Fails At The Desk
Built export in the main process, validating every scene and connector against a schema first. A story that would break in playback doesn't produce a file.
- Schema Validation
- Export
- Error Reporting
Phase 5: The Library
Several Stories At Once
Built the story list and drawer, because a studio has four things in progress, and switching between them shouldn't mean opening a different folder.
- Story List
- Switching
- Local Store
Playtest: one path through the story, with the state changing as it goes. Four scenes in, one line is skipped because trusts_tom is false, each step shows the flag it set, and the open choice marks where the playtest would stop.
Operational results after launch
No
Dangling references possible
None
Media uploaded
Before export
Validation runs
Zero
Errors found in studio
Dangling references possible is no, structurally: an edge is a reference and can't point at a scene that doesn't exist. Media uploaded is none. Validation runs before export. Errors found in studio is zero across the productions since: the class of fault that used to stop sessions can't reach one.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
About our collaboration
- 01
Two of the studio's writers used each build for a week before we moved on. The dialogue repeater exists because of that: our first version was a text block with a convention, and the writers ignored the convention within a day, exactly as they had in the documents.
Twelve weeks, fixed, with the graph model settled before any editor work: everything else follows from scenes being nodes and choices being edges. Writers used builds on a live production from week five, which is where the decision to validate at export instead of at load came from. A draft in progress has to be allowed to be incomplete.
What we'd carry into the next one
- 01
If the work is a graph, the tool should be a graph. A document forces the writer to hold the structure in their head.
When the work is a graph, a document makes the writer the validator: they hold the structure in their head, and the mistakes are the ones memory makes.
- 02
Make the broken state unrepresentable, not just detectable. A dangling edge you can't draw needs no validation rule.
An unrepresentable broken state needs no validation rule, no error message and no discipline, which is strictly better than detecting it well.
- 03
Validate at export instead of at load, so a work in progress is allowed to be incomplete.
Validating at load would have made every incomplete draft an error; validating at export lets the work in progress be exactly that.
- 04
Large unreleased media is a good enough reason on its own to build a desktop application.
Large unreleased media under embargo is on its own a sufficient reason to build a desktop application, before any argument about performance.
Play it, then export it
A draft can be unfinished. A bundle can’t.
Walk four scenes of Saltmarsh Signal and watch the flags change, then run the export check on the same draft and fix the two errors it names. Switch tabs, or use the arrow keys once one is focused.
The playtest starts at the first scene with every flag false. Take the highlighted choice to walk the path.
- NarratorTwo a.m. at the Saltmarsh relay station. The kettle has gone cold.
- Ines VargaChannel sixteen. Nobody should be on sixteen tonight.
- The Caller…anyone… relay station… can you hear me…
- radio_answeredInes picked up channel sixteenfalse
- heard_signalThe Caller's warning was heard outfalse
- has_coordinatesGrid seven is written downfalse
- trusts_tomTom was let infalse
- has_keyThe pump-house key is takenfalse
Why the check waits for export: a draft in progress has to be allowed to be incomplete, so an open choice is fine on the canvas and refused only when a file would be written.
From a connector on the canvas to a bundle that plays
The graph rules out a dangling choice as it's drawn. The export catches what a draft is allowed to leave unfinished, before a file exists, so the fault surfaces at a writer’s desk instead of in a recording session.
- 01 · SourceScene nodes and choice edges on the canvasThe branching shape is the thing on screen, and the layout a writer arranged is saved with the story.
- 02 · ModelStory graph: a choice is a referenceAn edge can't point at a scene that doesn't exist. Deleting a scene takes its incoming choices or is refused with them listed.
- 03 · StateLocal story file and media on diskAudio and images are local file references. Nothing uploads, and dialogue is stored as speaker-and-line records.
- 04 · EngineZod validation in the main processEvery scene is checked at export, never at load: required fields, every edge resolvable, every media file on disk.
- 05 · DeliveryBundle written for the playerWritten only when validation passes. A failure names the offending scene, and no file is produced.
Broken paths, lost media and failed bundles, stopped at the desk
Building an authoring tool where a broken story can’t reach the studio? Scope your build in 3 minutes.
Scope your buildNearby engagements
Web PlatformsA writing desk where the manuscript never leaves the machine
A desktop notebook built on Electron, where documents live on disk, the editor is a real rich-text surface, and the AI assistant reads only what the writer hands it.
Book Publishing · 14 weeks
AI & AutomationLong text becomes narrated audio, and the job tells you the truth while it runs
A production tool that pulls text out of a source, narrates it, and reports honestly on a job that takes minutes, without pretending to be instant.
Media & Publishing · 12 weeks
Web PlatformsTwo billing models in one product, and neither one pretending the other doesn't exist
A content platform sold both by subscription and as a lifetime deal, where a redeemed code and a monthly plan resolve to the same entitlement, with no parallel systems.
Media & Publishing · 18 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














