Vibecode Wireflow
track this build4 phases, 12 steps, beginner friendly0%Chaining a few model calls is a script, and ComfyUI already gives you a free node canvas. What you will not get in a weekend is the part after generation: a track and keyframe timeline with hosted rendering, and live multiplayer inside it. The graph is vibecodable. The editor attached to it is not.
You are building a lean indie version of Wireflow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Wireflow · indie build A local visual pipeline runner whose hard problems are never spending money by accident, surviving a restart with in-flight jobs intact, and keeping outputs after the provider's URLs have expired. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Frontend | Vite with React and React Flow in the same repository as the server | one repository, one command, no accounts, localhost only | | Server | Express with better-sqlite3, no authentication | a single-user local tool has no business having a login | | Provider | fal.ai through its queue API, one key in the environment | the queue gives you an identifier you can query from another process | | Models | Declared in models.json with the path and which fields are ports versus settings | adding a model should be a data change, not a code change | | Caching | Node outputs keyed by node id plus a hash of resolved inputs | a re-run should only pay for what actually changed | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 20 or newer** · free - Why: Runs both the Vite frontend and the Express server from one repository. - Get it: nodejs.org, or nvm. - Verify: node --version prints v20 or higher and both dev servers start - [ ] **A fal.ai API key with a funded balance** · pay per generation; check the current per-model prices before running a graph - Why: Every generate node costs money at submission, so this is the prerequisite that makes the ceiling work in the last phase meaningful rather than theoretical. - Get it: Create an account, add a key, and fund it with a small amount you are willing to lose while testing. - Verify: A single image generation submits, completes and returns a result - [ ] **Current per-model prices for the two models you ship** · free - Why: The cost estimate on the run plan is only useful if the figures behind it are real, and provider prices change without notice. - Get it: Read the provider's pricing for each model path in models.json and record the figure with the date checked. - Verify: models.json carries a price and a checked date for every entry - [ ] **A fake queue server for tests** · free - Why: Restart recovery, duplicate submission and ceiling behaviour all need a provider you can kill, stall and count calls against, and doing that against the real one costs money each time. - Get it: A small local server implementing submit, status and result with configurable delays. - Verify: The suite can submit, restart the server mid-job and observe recovery - [ ] **Your daily and per-run spend ceilings** · free - Why: They are asserted by tests and they refuse real submissions, so choosing them after an expensive evening means choosing whatever that evening cost. - Get it: Write down the most you are willing to spend in a day and in one run. - Verify: Both appear in the environment and are named in the ceiling tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. - a compositor and keyframe timeline, with hosted rendering - live multiplayer on the canvas and inside the editor - hundreds of model schemas kept current for you - paid steps that never fire by accident, and jobs that survive a closed tab - a library of ready-made pipelines to fork, and boards you can share If one of those is essential to you, that is the reason to keep paying for Wireflow, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Wireflow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local node canvas for chaining hosted media models, where nothing costs money until you press run to replace Wireflow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and the first one is the entire reason this build exists rather than a script. Generate nodes never run on edit. Not on a keystroke, not on a connection being made, not on a node being dropped on the canvas, not on the graph autosaving. Only the Run button and the per-node run button submit a job. Every reactive execution framework you might reach for will tempt you to wire generation into a state change, and the first time that fires in a loop it is a real invoice at a real provider. Put the rule in the README and assert it in a test that counts provider calls while the graph is edited. Second, persist the provider's request identifier before the first poll, not after it. A job submitted and then lost because the server restarted between submission and the first status check is a generation you paid for and cannot find, and the queue API exists precisely so that an identifier stored now can be queried later from a different process. ### Stack (fixed, do not substitute) - Frontend: Vite with React and React Flow in the same repository as the server · one repository, one command, no accounts, localhost only - Server: Express with better-sqlite3, no authentication · a single-user local tool has no business having a login - Provider: fal.ai through its queue API, one key in the environment · the queue gives you an identifier you can query from another process - Models: Declared in models.json with the path and which fields are ports versus settings · adding a model should be a data change, not a code change - Caching: Node outputs keyed by node id plus a hash of resolved inputs · a re-run should only pay for what actually changed ### Data model (create this before Phase 1) - graphs(id, name, nodes_json, edges_json, updated_at) · autosaved on every canvas change, and autosaving never triggers a run - nodes(graph_id, node_id, type, settings_json) · type is text, image_import, prompt_template, image_generate, video_generate or preview - runs(id, graph_id, started_at, finished_at, state, estimated_cost, nodes_executed, nodes_from_cache) - jobs(id, run_id, node_id, input_hash, provider, model_path, request_id, state, submitted_at, completed_at, error) · request_id is written before the first poll - outputs(node_id, input_hash, kind, local_path, provider_url, bytes, created_at) · unique on node id and input hash, which is what makes a re-run free - assets(sha256, path, mime, bytes) · imported images and downloaded results, stored locally because remote URLs expire - The cache key is the node id plus a hash of its resolved inputs, so changing a prompt three nodes upstream re-executes everything downstream of it and nothing else. ### Phase 1 · A canvas that saves and cannot spend Build: Build the React Flow canvas with a palette of the six node types, wiring outputs to inputs, pan and zoom, and an autosave to SQLite on every change. Load models.json and render each model's ports and settings from it. Do not implement any provider call in this phase at all. Trap: Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. Done when: No module imported by the canvas can reach the provider client, enforced by a dependency assertion in the test suite. Editing, connecting and moving nodes autosaves and a reload restores the graph exactly, including node positions. Ports and settings for each model are rendered from models.json without model-specific code. The six node types are the only ones available. Do not build yet: No running, hashing, submission or downloads. ### Phase 2 · Running on purpose, and knowing the price first Build: Topologically sort the graph, resolve each node's inputs, hash them, and execute layer by layer. A node whose input hash already has an output is a cache hit and is not executed. Before any submission, show a plan: which nodes will execute, which are cached, and the estimated cost from per-model figures in models.json with the date you checked them. Trap: Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. Done when: Changing an upstream text node invalidates every downstream node and no others, tested on a branching graph. Re-running an unchanged graph executes nothing and reports every node as cached. A cycle in the graph is refused before anything runs, with the cycle shown. The run plan lists nodes to execute, cached nodes and an estimated cost, and pressing run without viewing the plan is not possible. Do not build yet: No provider submission, polling or downloads yet. ### Phase 3 · Jobs that survive the server restarting Build: Submit through the provider's queue endpoint and write the returned request identifier to the jobs table before the first status poll. Poll with backoff and record the state transitions the provider reports. On startup, find every job not in a terminal state and resume polling it. A duplicate submission for the same input hash is refused while a job for it is in flight. Trap: Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. Done when: Killing the server after submission and restarting resumes polling and completes the job, tested against a fake queue. A second run of the same unchanged node while a job is in flight joins the existing job rather than submitting again, tested by counting submissions. Every job row has its request identifier written before the first poll, asserted by ordering in the test. Polling backs off and a provider error is stored on the job with its message. Do not build yet: No downloads, spend ceilings or export yet. ### Phase 4 · Keeping what you paid for Build: Download every result to local storage as soon as a job completes, store it by content hash, and point the preview nodes at the local copy. The provider's documentation is explicit that result media URLs are subject to an expiry setting and that you should download what you want to keep (checked while writing this), so a run is not marked complete until its assets are on disk. Trap: Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. Done when: A completed job has its asset on local disk before the run is marked finished, asserted in the test. Previews render from local paths and never request the provider's domain, verified with the network monitored. The same output produced twice is stored once by hash. A missing local asset renders an explicit missing state naming the run and node rather than a broken image. Do not build yet: No spend ceiling, run history or graph export yet. ### Phase 5 · Ceilings, history and handing the graph to someone else Build: A spend ledger recording every job with its estimated and, where the provider reports it, actual cost. A daily and per-run ceiling that refuses to submit when crossed, with the figure shown on the run plan. A run history showing every node, its input hash, its job and its output. Export a graph with its models.json entries and its imported assets, and import it into an empty install. Trap: A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. Done when: A run that would cross the daily ceiling stops before the submission that crosses it and says which node it stopped at, tested with a low ceiling. In-flight committed cost counts towards the ceiling, tested by submitting concurrently. The run history shows every node with its input hash, job identifier and output, and links a broken preview to its run. Exporting a graph and importing it into an empty install reproduces the canvas, the model entries and the imported assets. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer on the canvas, which needs a conflict resolution model and a server you operate, and which is a large part of what the paid product sells. - A compositor and keyframe timeline, which is a separate application and nobody builds one in a weekend. - Credits and billing, since you hold the provider key directly and pay the provider's own prices rather than a marked-up balance. - Iterators and fan-out over arrays, which multiply cost in exactly the way the first rule exists to prevent; add them only after the ceiling has proved itself. - A second provider, because the queue semantics, model schemas and cost reporting differ enough that it is a rewrite of three phases rather than an adapter. ### README must contain - That generate nodes never execute on edit, only on the run buttons, and that a test asserts it by counting provider calls during editing - That every run spends real money at the provider, with the estimate shown on the plan and the figures in models.json carrying the date they were checked - That the request identifier is stored before the first poll so an in-flight job survives a restart - That result media are downloaded locally before a run is marked finished, because the provider's URLs expire - How to add a third model by editing models.json, with no code change ===== AGENTS.md ===== # Agent instructions · Wireflow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with React and React Flow in the same repository as the server, Express with better-sqlite3, no authentication, fal.ai through its queue API, one key in the environment, Declared in models.json with the path and which fields are ports versus settings, Node outputs keyed by node id plus a hash of resolved inputs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ===== BUILD_PLAN.md ===== # Build plan · Wireflow A local visual pipeline runner whose hard problems are never spending money by accident, surviving a restart with in-flight jobs intact, and keeping outputs after the provider's URLs have expired. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A canvas that saves and cannot spend The graph edits, autosaves and reloads, and no code path can call the provider yet. ### Steps 1. Build the canvas with autosave to SQLite Every change persists; none of them executes anything. Files: `src/canvas/Graph.tsx`, `server/db/schema.sql` 2. Render ports and settings from models.json Adding a model must never mean writing a component. Files: `models.json`, `src/canvas/NodeFromModel.tsx` 3. Assert the canvas cannot import the provider client A dependency test is the cheapest version of this rule. Files: `tests/no-provider-in-ui.test.ts` ### Done when - [ ] The canvas cannot import the provider client - [ ] A reload restores the graph including positions - [ ] Ports and settings come from models.json - [ ] Only the six node types exist ### Watch out - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. ## Phase 2 · Running on purpose, and knowing the price first Run executes a topologically sorted graph layer by layer, reusing cached outputs, after showing what it will cost. ### Steps 1. Hash resolved inputs, not node settings The difference between a correct cache and a confusing one. Files: `server/run/hash.ts`, `tests/cache-invalidation.test.ts` 2. Refuse a cyclic graph before executing A cycle plus a generate node is an unbounded bill. Files: `server/run/topo.ts` 3. Show the plan and the estimate before submitting The last moment where the run is still free. Files: `src/run/Plan.tsx`, `models.json` ### Done when - [ ] An upstream change invalidates exactly the nodes below it - [ ] An unchanged re-run executes nothing - [ ] A cycle is refused with the cycle shown - [ ] The plan and cost estimate precede every run ### Watch out - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. ## Phase 3 · Jobs that survive the server restarting A submitted job is recoverable from its identifier alone, from a cold start. ### Steps 1. Write the request identifier before polling The order is the whole guarantee. Files: `server/provider/submit.ts`, `tests/restart-recovery.test.ts` 2. Resume unfinished jobs at startup The table is the record; the poller is not. Files: `server/run/resume.ts` 3. Join an in-flight job rather than resubmitting Two clicks on run should cost what one costs. Files: `server/run/execute.ts` ### Done when - [ ] A restart mid-job resumes and completes it - [ ] A duplicate run joins the in-flight job - [ ] The identifier is written before the first poll - [ ] Provider errors are stored on the job ### Watch out - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. ## Phase 4 · Keeping what you paid for Every output lives on your disk before the run is called finished. ### Steps 1. Download before marking the run complete Complete should mean the bytes are yours. Files: `server/assets/download.ts`, `tests/assets.test.ts` 2. Store assets by content hash Identical outputs cost one copy. Files: `server/assets/store.ts` 3. Render previews from local paths only A preview that hits the provider is a preview that expires. Files: `src/nodes/Preview.tsx` ### Done when - [ ] Assets are on disk before a run is marked finished - [ ] Previews never request the provider domain - [ ] Duplicate outputs are stored once - [ ] A missing asset renders an explicit state ### Watch out - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ## Not in this build - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. ## After v1, if you want it - A third and fourth model added to models.json, which should need no code at all if Phase 1 was done properly - Fan-out over a list of prompts, but only once the ceiling has stopped a real run at least once - Seed pinning and a side-by-side comparison of two runs, which the input hash already makes possible ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The provider key. Server side only; the canvas never sees it. FAL_KEY=fal-...redacted # Required. Graphs, runs, jobs and outputs. DATABASE_PATH=./data/wireflow.db # Required. Imported images and downloaded results, stored by content hash. STORAGE_DIR=./storage # Required. Model paths, port definitions and prices with the date each was checked. MODELS_FILE=./models.json # Required. Checked before every submission, counting jobs already in flight. DAILY_SPEND_CEILING=5.00 # Required. Per run. Crossing it stops at the submission that would cross it. RUN_SPEND_CEILING=1.00 # Required. How long a job is polled before it is marked as needing manual checking. POLL_MAX_SECONDS=900
You are building a lean indie version of Wireflow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Wireflow · indie build A local visual pipeline runner whose hard problems are never spending money by accident, surviving a restart with in-flight jobs intact, and keeping outputs after the provider's URLs have expired. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Frontend | Vite with React and React Flow in the same repository as the server | one repository, one command, no accounts, localhost only | | Server | Express with better-sqlite3, no authentication | a single-user local tool has no business having a login | | Provider | fal.ai through its queue API, one key in the environment | the queue gives you an identifier you can query from another process | | Models | Declared in models.json with the path and which fields are ports versus settings | adding a model should be a data change, not a code change | | Caching | Node outputs keyed by node id plus a hash of resolved inputs | a re-run should only pay for what actually changed | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 20 or newer** · free - Why: Runs both the Vite frontend and the Express server from one repository. - Get it: nodejs.org, or nvm. - Verify: node --version prints v20 or higher and both dev servers start - [ ] **A fal.ai API key with a funded balance** · pay per generation; check the current per-model prices before running a graph - Why: Every generate node costs money at submission, so this is the prerequisite that makes the ceiling work in the last phase meaningful rather than theoretical. - Get it: Create an account, add a key, and fund it with a small amount you are willing to lose while testing. - Verify: A single image generation submits, completes and returns a result - [ ] **Current per-model prices for the two models you ship** · free - Why: The cost estimate on the run plan is only useful if the figures behind it are real, and provider prices change without notice. - Get it: Read the provider's pricing for each model path in models.json and record the figure with the date checked. - Verify: models.json carries a price and a checked date for every entry - [ ] **A fake queue server for tests** · free - Why: Restart recovery, duplicate submission and ceiling behaviour all need a provider you can kill, stall and count calls against, and doing that against the real one costs money each time. - Get it: A small local server implementing submit, status and result with configurable delays. - Verify: The suite can submit, restart the server mid-job and observe recovery - [ ] **Your daily and per-run spend ceilings** · free - Why: They are asserted by tests and they refuse real submissions, so choosing them after an expensive evening means choosing whatever that evening cost. - Get it: Write down the most you are willing to spend in a day and in one run. - Verify: Both appear in the environment and are named in the ceiling tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. - a compositor and keyframe timeline, with hosted rendering - live multiplayer on the canvas and inside the editor - hundreds of model schemas kept current for you - paid steps that never fire by accident, and jobs that survive a closed tab - a library of ready-made pipelines to fork, and boards you can share If one of those is essential to you, that is the reason to keep paying for Wireflow, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Wireflow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local node canvas for chaining hosted media models, where nothing costs money until you press run to replace Wireflow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and the first one is the entire reason this build exists rather than a script. Generate nodes never run on edit. Not on a keystroke, not on a connection being made, not on a node being dropped on the canvas, not on the graph autosaving. Only the Run button and the per-node run button submit a job. Every reactive execution framework you might reach for will tempt you to wire generation into a state change, and the first time that fires in a loop it is a real invoice at a real provider. Put the rule in the README and assert it in a test that counts provider calls while the graph is edited. Second, persist the provider's request identifier before the first poll, not after it. A job submitted and then lost because the server restarted between submission and the first status check is a generation you paid for and cannot find, and the queue API exists precisely so that an identifier stored now can be queried later from a different process. ### Stack (fixed, do not substitute) - Frontend: Vite with React and React Flow in the same repository as the server · one repository, one command, no accounts, localhost only - Server: Express with better-sqlite3, no authentication · a single-user local tool has no business having a login - Provider: fal.ai through its queue API, one key in the environment · the queue gives you an identifier you can query from another process - Models: Declared in models.json with the path and which fields are ports versus settings · adding a model should be a data change, not a code change - Caching: Node outputs keyed by node id plus a hash of resolved inputs · a re-run should only pay for what actually changed ### Data model (create this before Phase 1) - graphs(id, name, nodes_json, edges_json, updated_at) · autosaved on every canvas change, and autosaving never triggers a run - nodes(graph_id, node_id, type, settings_json) · type is text, image_import, prompt_template, image_generate, video_generate or preview - runs(id, graph_id, started_at, finished_at, state, estimated_cost, nodes_executed, nodes_from_cache) - jobs(id, run_id, node_id, input_hash, provider, model_path, request_id, state, submitted_at, completed_at, error) · request_id is written before the first poll - outputs(node_id, input_hash, kind, local_path, provider_url, bytes, created_at) · unique on node id and input hash, which is what makes a re-run free - assets(sha256, path, mime, bytes) · imported images and downloaded results, stored locally because remote URLs expire - The cache key is the node id plus a hash of its resolved inputs, so changing a prompt three nodes upstream re-executes everything downstream of it and nothing else. ### Phase 1 · A canvas that saves and cannot spend Build: Build the React Flow canvas with a palette of the six node types, wiring outputs to inputs, pan and zoom, and an autosave to SQLite on every change. Load models.json and render each model's ports and settings from it. Do not implement any provider call in this phase at all. Trap: Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. Done when: No module imported by the canvas can reach the provider client, enforced by a dependency assertion in the test suite. Editing, connecting and moving nodes autosaves and a reload restores the graph exactly, including node positions. Ports and settings for each model are rendered from models.json without model-specific code. The six node types are the only ones available. Do not build yet: No running, hashing, submission or downloads. ### Phase 2 · Running on purpose, and knowing the price first Build: Topologically sort the graph, resolve each node's inputs, hash them, and execute layer by layer. A node whose input hash already has an output is a cache hit and is not executed. Before any submission, show a plan: which nodes will execute, which are cached, and the estimated cost from per-model figures in models.json with the date you checked them. Trap: Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. Done when: Changing an upstream text node invalidates every downstream node and no others, tested on a branching graph. Re-running an unchanged graph executes nothing and reports every node as cached. A cycle in the graph is refused before anything runs, with the cycle shown. The run plan lists nodes to execute, cached nodes and an estimated cost, and pressing run without viewing the plan is not possible. Do not build yet: No provider submission, polling or downloads yet. ### Phase 3 · Jobs that survive the server restarting Build: Submit through the provider's queue endpoint and write the returned request identifier to the jobs table before the first status poll. Poll with backoff and record the state transitions the provider reports. On startup, find every job not in a terminal state and resume polling it. A duplicate submission for the same input hash is refused while a job for it is in flight. Trap: Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. Done when: Killing the server after submission and restarting resumes polling and completes the job, tested against a fake queue. A second run of the same unchanged node while a job is in flight joins the existing job rather than submitting again, tested by counting submissions. Every job row has its request identifier written before the first poll, asserted by ordering in the test. Polling backs off and a provider error is stored on the job with its message. Do not build yet: No downloads, spend ceilings or export yet. ### Phase 4 · Keeping what you paid for Build: Download every result to local storage as soon as a job completes, store it by content hash, and point the preview nodes at the local copy. The provider's documentation is explicit that result media URLs are subject to an expiry setting and that you should download what you want to keep (checked while writing this), so a run is not marked complete until its assets are on disk. Trap: Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. Done when: A completed job has its asset on local disk before the run is marked finished, asserted in the test. Previews render from local paths and never request the provider's domain, verified with the network monitored. The same output produced twice is stored once by hash. A missing local asset renders an explicit missing state naming the run and node rather than a broken image. Do not build yet: No spend ceiling, run history or graph export yet. ### Phase 5 · Ceilings, history and handing the graph to someone else Build: A spend ledger recording every job with its estimated and, where the provider reports it, actual cost. A daily and per-run ceiling that refuses to submit when crossed, with the figure shown on the run plan. A run history showing every node, its input hash, its job and its output. Export a graph with its models.json entries and its imported assets, and import it into an empty install. Trap: A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. Done when: A run that would cross the daily ceiling stops before the submission that crosses it and says which node it stopped at, tested with a low ceiling. In-flight committed cost counts towards the ceiling, tested by submitting concurrently. The run history shows every node with its input hash, job identifier and output, and links a broken preview to its run. Exporting a graph and importing it into an empty install reproduces the canvas, the model entries and the imported assets. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer on the canvas, which needs a conflict resolution model and a server you operate, and which is a large part of what the paid product sells. - A compositor and keyframe timeline, which is a separate application and nobody builds one in a weekend. - Credits and billing, since you hold the provider key directly and pay the provider's own prices rather than a marked-up balance. - Iterators and fan-out over arrays, which multiply cost in exactly the way the first rule exists to prevent; add them only after the ceiling has proved itself. - A second provider, because the queue semantics, model schemas and cost reporting differ enough that it is a rewrite of three phases rather than an adapter. ### README must contain - That generate nodes never execute on edit, only on the run buttons, and that a test asserts it by counting provider calls during editing - That every run spends real money at the provider, with the estimate shown on the plan and the figures in models.json carrying the date they were checked - That the request identifier is stored before the first poll so an in-flight job survives a restart - That result media are downloaded locally before a run is marked finished, because the provider's URLs expire - How to add a third model by editing models.json, with no code change ===== AGENTS.md ===== # Agent instructions · Wireflow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with React and React Flow in the same repository as the server, Express with better-sqlite3, no authentication, fal.ai through its queue API, one key in the environment, Declared in models.json with the path and which fields are ports versus settings, Node outputs keyed by node id plus a hash of resolved inputs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ===== BUILD_PLAN.md ===== # Build plan · Wireflow A local visual pipeline runner whose hard problems are never spending money by accident, surviving a restart with in-flight jobs intact, and keeping outputs after the provider's URLs have expired. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A canvas that saves and cannot spend The graph edits, autosaves and reloads, and no code path can call the provider yet. ### Steps 1. Build the canvas with autosave to SQLite Every change persists; none of them executes anything. Files: `src/canvas/Graph.tsx`, `server/db/schema.sql` 2. Render ports and settings from models.json Adding a model must never mean writing a component. Files: `models.json`, `src/canvas/NodeFromModel.tsx` 3. Assert the canvas cannot import the provider client A dependency test is the cheapest version of this rule. Files: `tests/no-provider-in-ui.test.ts` ### Done when - [ ] The canvas cannot import the provider client - [ ] A reload restores the graph including positions - [ ] Ports and settings come from models.json - [ ] Only the six node types exist ### Watch out - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. ## Phase 2 · Running on purpose, and knowing the price first Run executes a topologically sorted graph layer by layer, reusing cached outputs, after showing what it will cost. ### Steps 1. Hash resolved inputs, not node settings The difference between a correct cache and a confusing one. Files: `server/run/hash.ts`, `tests/cache-invalidation.test.ts` 2. Refuse a cyclic graph before executing A cycle plus a generate node is an unbounded bill. Files: `server/run/topo.ts` 3. Show the plan and the estimate before submitting The last moment where the run is still free. Files: `src/run/Plan.tsx`, `models.json` ### Done when - [ ] An upstream change invalidates exactly the nodes below it - [ ] An unchanged re-run executes nothing - [ ] A cycle is refused with the cycle shown - [ ] The plan and cost estimate precede every run ### Watch out - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. ## Phase 3 · Jobs that survive the server restarting A submitted job is recoverable from its identifier alone, from a cold start. ### Steps 1. Write the request identifier before polling The order is the whole guarantee. Files: `server/provider/submit.ts`, `tests/restart-recovery.test.ts` 2. Resume unfinished jobs at startup The table is the record; the poller is not. Files: `server/run/resume.ts` 3. Join an in-flight job rather than resubmitting Two clicks on run should cost what one costs. Files: `server/run/execute.ts` ### Done when - [ ] A restart mid-job resumes and completes it - [ ] A duplicate run joins the in-flight job - [ ] The identifier is written before the first poll - [ ] Provider errors are stored on the job ### Watch out - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. ## Phase 4 · Keeping what you paid for Every output lives on your disk before the run is called finished. ### Steps 1. Download before marking the run complete Complete should mean the bytes are yours. Files: `server/assets/download.ts`, `tests/assets.test.ts` 2. Store assets by content hash Identical outputs cost one copy. Files: `server/assets/store.ts` 3. Render previews from local paths only A preview that hits the provider is a preview that expires. Files: `src/nodes/Preview.tsx` ### Done when - [ ] Assets are on disk before a run is marked finished - [ ] Previews never request the provider domain - [ ] Duplicate outputs are stored once - [ ] A missing asset renders an explicit state ### Watch out - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ## Not in this build - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. ## After v1, if you want it - A third and fourth model added to models.json, which should need no code at all if Phase 1 was done properly - Fan-out over a list of prompts, but only once the ceiling has stopped a real run at least once - Seed pinning and a side-by-side comparison of two runs, which the input hash already makes possible ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The provider key. Server side only; the canvas never sees it. FAL_KEY=fal-...redacted # Required. Graphs, runs, jobs and outputs. DATABASE_PATH=./data/wireflow.db # Required. Imported images and downloaded results, stored by content hash. STORAGE_DIR=./storage # Required. Model paths, port definitions and prices with the date each was checked. MODELS_FILE=./models.json # Required. Checked before every submission, counting jobs already in flight. DAILY_SPEND_CEILING=5.00 # Required. Per run. Crossing it stops at the submission that would cross it. RUN_SPEND_CEILING=1.00 # Required. How long a job is polled before it is marked as needing manual checking. POLL_MAX_SECONDS=900
You are building a production product version of Wireflow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Wireflow · product brief ## Problem Chaining a few model calls is a script, and ComfyUI already gives you a free node canvas. What you will not get in a weekend is the part after generation: a track and keyframe timeline with hosted rendering, and live multiplayer inside it. The graph is vibecodable. The editor attached to it is not. ## Product outcome A node canvas you can edit all day for free, that tells you what a run will cost before it starts, stops at the ceiling instead of after it, recovers jobs across a restart, and keeps every output on your own disk. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - fal.ai API key or another hosted model provider - Node 20 - somewhere to run it that stays up while jobs finish - a GPU only if you self-host the models instead ## Explicit non-goals for v1 - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. - a compositor and keyframe timeline, with hosted rendering - live multiplayer on the canvas and inside the editor - hundreds of model schemas kept current for you - paid steps that never fire by accident, and jobs that survive a closed tab - a library of ready-made pipelines to fork, and boards you can share ## Success criteria - The canvas is confirmed unable to import the provider client - An upstream change is confirmed to invalidate exactly the nodes below it - A restart mid-job is confirmed to resume and complete it - Assets are confirmed on disk before a run is marked finished - A run is confirmed to stop at the submission that would cross the ceiling ===== BRIEF.md ===== # Build brief · Wireflow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local node canvas for chaining hosted media models, where nothing costs money until you press run to replace Wireflow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and the first one is the entire reason this build exists rather than a script. Generate nodes never run on edit. Not on a keystroke, not on a connection being made, not on a node being dropped on the canvas, not on the graph autosaving. Only the Run button and the per-node run button submit a job. Every reactive execution framework you might reach for will tempt you to wire generation into a state change, and the first time that fires in a loop it is a real invoice at a real provider. Put the rule in the README and assert it in a test that counts provider calls while the graph is edited. Second, persist the provider's request identifier before the first poll, not after it. A job submitted and then lost because the server restarted between submission and the first status check is a generation you paid for and cannot find, and the queue API exists precisely so that an identifier stored now can be queried later from a different process. ### Stack (fixed, do not substitute) - Frontend: Vite with React and React Flow in the same repository as the server · one repository, one command, no accounts, localhost only - Server: Express with better-sqlite3, no authentication · a single-user local tool has no business having a login - Provider: fal.ai through its queue API, one key in the environment · the queue gives you an identifier you can query from another process - Models: Declared in models.json with the path and which fields are ports versus settings · adding a model should be a data change, not a code change - Caching: Node outputs keyed by node id plus a hash of resolved inputs · a re-run should only pay for what actually changed ### Data model (create this before Phase 1) - graphs(id, name, nodes_json, edges_json, updated_at) · autosaved on every canvas change, and autosaving never triggers a run - nodes(graph_id, node_id, type, settings_json) · type is text, image_import, prompt_template, image_generate, video_generate or preview - runs(id, graph_id, started_at, finished_at, state, estimated_cost, nodes_executed, nodes_from_cache) - jobs(id, run_id, node_id, input_hash, provider, model_path, request_id, state, submitted_at, completed_at, error) · request_id is written before the first poll - outputs(node_id, input_hash, kind, local_path, provider_url, bytes, created_at) · unique on node id and input hash, which is what makes a re-run free - assets(sha256, path, mime, bytes) · imported images and downloaded results, stored locally because remote URLs expire - The cache key is the node id plus a hash of its resolved inputs, so changing a prompt three nodes upstream re-executes everything downstream of it and nothing else. ### Phase 1 · A canvas that saves and cannot spend Build: Build the React Flow canvas with a palette of the six node types, wiring outputs to inputs, pan and zoom, and an autosave to SQLite on every change. Load models.json and render each model's ports and settings from it. Do not implement any provider call in this phase at all. Trap: Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. Done when: No module imported by the canvas can reach the provider client, enforced by a dependency assertion in the test suite. Editing, connecting and moving nodes autosaves and a reload restores the graph exactly, including node positions. Ports and settings for each model are rendered from models.json without model-specific code. The six node types are the only ones available. Do not build yet: No running, hashing, submission or downloads. ### Phase 2 · Running on purpose, and knowing the price first Build: Topologically sort the graph, resolve each node's inputs, hash them, and execute layer by layer. A node whose input hash already has an output is a cache hit and is not executed. Before any submission, show a plan: which nodes will execute, which are cached, and the estimated cost from per-model figures in models.json with the date you checked them. Trap: Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. Done when: Changing an upstream text node invalidates every downstream node and no others, tested on a branching graph. Re-running an unchanged graph executes nothing and reports every node as cached. A cycle in the graph is refused before anything runs, with the cycle shown. The run plan lists nodes to execute, cached nodes and an estimated cost, and pressing run without viewing the plan is not possible. Do not build yet: No provider submission, polling or downloads yet. ### Phase 3 · Jobs that survive the server restarting Build: Submit through the provider's queue endpoint and write the returned request identifier to the jobs table before the first status poll. Poll with backoff and record the state transitions the provider reports. On startup, find every job not in a terminal state and resume polling it. A duplicate submission for the same input hash is refused while a job for it is in flight. Trap: Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. Done when: Killing the server after submission and restarting resumes polling and completes the job, tested against a fake queue. A second run of the same unchanged node while a job is in flight joins the existing job rather than submitting again, tested by counting submissions. Every job row has its request identifier written before the first poll, asserted by ordering in the test. Polling backs off and a provider error is stored on the job with its message. Do not build yet: No downloads, spend ceilings or export yet. ### Phase 4 · Keeping what you paid for Build: Download every result to local storage as soon as a job completes, store it by content hash, and point the preview nodes at the local copy. The provider's documentation is explicit that result media URLs are subject to an expiry setting and that you should download what you want to keep (checked while writing this), so a run is not marked complete until its assets are on disk. Trap: Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. Done when: A completed job has its asset on local disk before the run is marked finished, asserted in the test. Previews render from local paths and never request the provider's domain, verified with the network monitored. The same output produced twice is stored once by hash. A missing local asset renders an explicit missing state naming the run and node rather than a broken image. Do not build yet: No spend ceiling, run history or graph export yet. ### Phase 5 · Ceilings, history and handing the graph to someone else Build: A spend ledger recording every job with its estimated and, where the provider reports it, actual cost. A daily and per-run ceiling that refuses to submit when crossed, with the figure shown on the run plan. A run history showing every node, its input hash, its job and its output. Export a graph with its models.json entries and its imported assets, and import it into an empty install. Trap: A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. Done when: A run that would cross the daily ceiling stops before the submission that crosses it and says which node it stopped at, tested with a low ceiling. In-flight committed cost counts towards the ceiling, tested by submitting concurrently. The run history shows every node with its input hash, job identifier and output, and links a broken preview to its run. Exporting a graph and importing it into an empty install reproduces the canvas, the model entries and the imported assets. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer on the canvas, which needs a conflict resolution model and a server you operate, and which is a large part of what the paid product sells. - A compositor and keyframe timeline, which is a separate application and nobody builds one in a weekend. - Credits and billing, since you hold the provider key directly and pay the provider's own prices rather than a marked-up balance. - Iterators and fan-out over arrays, which multiply cost in exactly the way the first rule exists to prevent; add them only after the ceiling has proved itself. - A second provider, because the queue semantics, model schemas and cost reporting differ enough that it is a rewrite of three phases rather than an adapter. ### README must contain - That generate nodes never execute on edit, only on the run buttons, and that a test asserts it by counting provider calls during editing - That every run spends real money at the provider, with the estimate shown on the plan and the figures in models.json carrying the date they were checked - That the request identifier is stored before the first poll so an in-flight job survives a restart - That result media are downloaded locally before a run is marked finished, because the provider's URLs expire - How to add a third model by editing models.json, with no code change ===== ARCHITECTURE.md ===== # Architecture · Wireflow ## Stack | Part | Choice | Why | | --- | --- | --- | | Frontend | Vite with React and React Flow in the same repository as the server | one repository, one command, no accounts, localhost only | | Server | Express with better-sqlite3, no authentication | a single-user local tool has no business having a login | | Provider | fal.ai through its queue API, one key in the environment | the queue gives you an identifier you can query from another process | | Models | Declared in models.json with the path and which fields are ports versus settings | adding a model should be a data change, not a code change | | Caching | Node outputs keyed by node id plus a hash of resolved inputs | a re-run should only pay for what actually changed | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Canvas | Editing, autosave and rendering nodes from models.json. | Any canvas library, provided it cannot reach the provider client. | | Runner | Topological order, input hashing and cache hits. | Any executor, provided hashing covers resolved inputs. | | Provider | Queue submission, identifier persistence and polling. | Any provider with a queue, provided the identifier is stored before the first poll. | | Assets | Downloads, content-addressed storage and previews. | Any store, provided a run is not complete until the bytes are local. | | Cost | Estimates, the ledger and the ceilings. | Not swappable for a per-run check, which fan-out defeats. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `FAL_KEY` · required, secret · The provider key. Server side only; the canvas never sees it. - `DATABASE_PATH` · required · Graphs, runs, jobs and outputs. - `STORAGE_DIR` · required · Imported images and downloaded results, stored by content hash. - `MODELS_FILE` · required · Model paths, port definitions and prices with the date each was checked. - `DAILY_SPEND_CEILING` · required · Checked before every submission, counting jobs already in flight. - `RUN_SPEND_CEILING` · required · Per run. Crossing it stops at the submission that would cross it. - `POLL_MAX_SECONDS` · required · How long a job is polled before it is marked as needing manual checking. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Wireflow product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite with React and React Flow in the same repository as the server, Express with better-sqlite3, no authentication, fal.ai through its queue API, one key in the environment, Declared in models.json with the path and which fields are ports versus settings, Node outputs keyed by node id plus a hash of resolved inputs. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. - A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. ===== MILESTONES.md ===== # Delivery milestones · Wireflow Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A canvas that saves and cannot spend The graph edits, autosaves and reloads, and no code path can call the provider yet. ### Steps 1. Build the canvas with autosave to SQLite Every change persists; none of them executes anything. Files: `src/canvas/Graph.tsx`, `server/db/schema.sql` 2. Render ports and settings from models.json Adding a model must never mean writing a component. Files: `models.json`, `src/canvas/NodeFromModel.tsx` 3. Assert the canvas cannot import the provider client A dependency test is the cheapest version of this rule. Files: `tests/no-provider-in-ui.test.ts` ### Done when - [ ] The canvas cannot import the provider client - [ ] A reload restores the graph including positions - [ ] Ports and settings come from models.json - [ ] Only the six node types exist ### Watch out - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. ## M2 · Running on purpose, and knowing the price first Run executes a topologically sorted graph layer by layer, reusing cached outputs, after showing what it will cost. ### Steps 1. Hash resolved inputs, not node settings The difference between a correct cache and a confusing one. Files: `server/run/hash.ts`, `tests/cache-invalidation.test.ts` 2. Refuse a cyclic graph before executing A cycle plus a generate node is an unbounded bill. Files: `server/run/topo.ts` 3. Show the plan and the estimate before submitting The last moment where the run is still free. Files: `src/run/Plan.tsx`, `models.json` ### Done when - [ ] An upstream change invalidates exactly the nodes below it - [ ] An unchanged re-run executes nothing - [ ] A cycle is refused with the cycle shown - [ ] The plan and cost estimate precede every run ### Watch out - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. ## M3 · Jobs that survive the server restarting A submitted job is recoverable from its identifier alone, from a cold start. ### Steps 1. Write the request identifier before polling The order is the whole guarantee. Files: `server/provider/submit.ts`, `tests/restart-recovery.test.ts` 2. Resume unfinished jobs at startup The table is the record; the poller is not. Files: `server/run/resume.ts` 3. Join an in-flight job rather than resubmitting Two clicks on run should cost what one costs. Files: `server/run/execute.ts` ### Done when - [ ] A restart mid-job resumes and completes it - [ ] A duplicate run joins the in-flight job - [ ] The identifier is written before the first poll - [ ] Provider errors are stored on the job ### Watch out - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. ## M4 · Keeping what you paid for Every output lives on your disk before the run is called finished. ### Steps 1. Download before marking the run complete Complete should mean the bytes are yours. Files: `server/assets/download.ts`, `tests/assets.test.ts` 2. Store assets by content hash Identical outputs cost one copy. Files: `server/assets/store.ts` 3. Render previews from local paths only A preview that hits the provider is a preview that expires. Files: `src/nodes/Preview.tsx` ### Done when - [ ] Assets are on disk before a run is marked finished - [ ] Previews never request the provider domain - [ ] Duplicate outputs are stored once - [ ] A missing asset renders an explicit state ### Watch out - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ## M5 · Ceilings, history and handing the graph to someone else (production only) Spending is bounded and visible, and a graph plus its models is portable. ### Steps 1. Check the ceiling before every submission Once per run is not a ceiling, it is a greeting. Files: `server/cost/ceiling.ts`, `tests/ceiling.test.ts` 2. Record estimated and reported cost per job The gap between the two is what you learn from. Files: `server/cost/ledger.ts` 3. Export the graph with its models and assets A graph without its model entries is not reproducible. Files: `server/export.ts`, `tests/graph-roundtrip.test.ts` ### Done when - [ ] A run stops at the submission that would cross the ceiling - [ ] In-flight cost counts towards the ceiling - [ ] Run history links nodes, hashes, jobs and outputs - [ ] A graph export reimports into an empty install ### Watch out - A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. ===== OPERATIONS.md ===== # Operations · Wireflow ## Backup The storage directory is the expensive part, since every file in it was paid for and cannot be regenerated at the same seed and price. Back it up more carefully than the database, which mainly holds graphs you could redraw. ## Restore Restore both, then mark any job not in a terminal state as needing manual checking rather than resubmitting it, and look those identifiers up at the provider before spending anything again. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Spend today against the ceiling, jobs polled past the maximum, outputs whose local asset is missing, and the age of the prices in models.json. A price checked eight months ago makes every estimate decorative. ## Incident checklist A surprise bill means either a ceiling was checked once per run or something executed on edit, and the jobs table with its submitted timestamps shows which. A missing output means a download failed after completion, and the request identifier is still in the jobs table to fetch it again while the provider's copy lasts. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The canvas is confirmed unable to import the provider client - [ ] An upstream change is confirmed to invalidate exactly the nodes below it - [ ] A restart mid-job is confirmed to resume and complete it - [ ] Assets are confirmed on disk before a run is marked finished - [ ] A run is confirmed to stop at the submission that would cross the ceiling ## Launch constraint Do not market omitted Wireflow capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The provider key. Server side only; the canvas never sees it. FAL_KEY=fal-...redacted # Required. Graphs, runs, jobs and outputs. DATABASE_PATH=./data/wireflow.db # Required. Imported images and downloaded results, stored by content hash. STORAGE_DIR=./storage # Required. Model paths, port definitions and prices with the date each was checked. MODELS_FILE=./models.json # Required. Checked before every submission, counting jobs already in flight. DAILY_SPEND_CEILING=5.00 # Required. Per run. Crossing it stops at the submission that would cross it. RUN_SPEND_CEILING=1.00 # Required. How long a job is polled before it is marked as needing manual checking. POLL_MAX_SECONDS=900
# Wireflow · indie build A local visual pipeline runner whose hard problems are never spending money by accident, surviving a restart with in-flight jobs intact, and keeping outputs after the provider's URLs have expired. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Frontend | Vite with React and React Flow in the same repository as the server | one repository, one command, no accounts, localhost only | | Server | Express with better-sqlite3, no authentication | a single-user local tool has no business having a login | | Provider | fal.ai through its queue API, one key in the environment | the queue gives you an identifier you can query from another process | | Models | Declared in models.json with the path and which fields are ports versus settings | adding a model should be a data change, not a code change | | Caching | Node outputs keyed by node id plus a hash of resolved inputs | a re-run should only pay for what actually changed | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 20 or newer** · free - Why: Runs both the Vite frontend and the Express server from one repository. - Get it: nodejs.org, or nvm. - Verify: node --version prints v20 or higher and both dev servers start - [ ] **A fal.ai API key with a funded balance** · pay per generation; check the current per-model prices before running a graph - Why: Every generate node costs money at submission, so this is the prerequisite that makes the ceiling work in the last phase meaningful rather than theoretical. - Get it: Create an account, add a key, and fund it with a small amount you are willing to lose while testing. - Verify: A single image generation submits, completes and returns a result - [ ] **Current per-model prices for the two models you ship** · free - Why: The cost estimate on the run plan is only useful if the figures behind it are real, and provider prices change without notice. - Get it: Read the provider's pricing for each model path in models.json and record the figure with the date checked. - Verify: models.json carries a price and a checked date for every entry - [ ] **A fake queue server for tests** · free - Why: Restart recovery, duplicate submission and ceiling behaviour all need a provider you can kill, stall and count calls against, and doing that against the real one costs money each time. - Get it: A small local server implementing submit, status and result with configurable delays. - Verify: The suite can submit, restart the server mid-job and observe recovery - [ ] **Your daily and per-run spend ceilings** · free - Why: They are asserted by tests and they refuse real submissions, so choosing them after an expensive evening means choosing whatever that evening cost. - Get it: Write down the most you are willing to spend in a day and in one run. - Verify: Both appear in the environment and are named in the ceiling tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. - a compositor and keyframe timeline, with hosted rendering - live multiplayer on the canvas and inside the editor - hundreds of model schemas kept current for you - paid steps that never fire by accident, and jobs that survive a closed tab - a library of ready-made pipelines to fork, and boards you can share If one of those is essential to you, that is the reason to keep paying for Wireflow, and the README should say so rather than pretend.
# Build brief · Wireflow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local node canvas for chaining hosted media models, where nothing costs money until you press run to replace Wireflow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and the first one is the entire reason this build exists rather than a script. Generate nodes never run on edit. Not on a keystroke, not on a connection being made, not on a node being dropped on the canvas, not on the graph autosaving. Only the Run button and the per-node run button submit a job. Every reactive execution framework you might reach for will tempt you to wire generation into a state change, and the first time that fires in a loop it is a real invoice at a real provider. Put the rule in the README and assert it in a test that counts provider calls while the graph is edited. Second, persist the provider's request identifier before the first poll, not after it. A job submitted and then lost because the server restarted between submission and the first status check is a generation you paid for and cannot find, and the queue API exists precisely so that an identifier stored now can be queried later from a different process. ### Stack (fixed, do not substitute) - Frontend: Vite with React and React Flow in the same repository as the server · one repository, one command, no accounts, localhost only - Server: Express with better-sqlite3, no authentication · a single-user local tool has no business having a login - Provider: fal.ai through its queue API, one key in the environment · the queue gives you an identifier you can query from another process - Models: Declared in models.json with the path and which fields are ports versus settings · adding a model should be a data change, not a code change - Caching: Node outputs keyed by node id plus a hash of resolved inputs · a re-run should only pay for what actually changed ### Data model (create this before Phase 1) - graphs(id, name, nodes_json, edges_json, updated_at) · autosaved on every canvas change, and autosaving never triggers a run - nodes(graph_id, node_id, type, settings_json) · type is text, image_import, prompt_template, image_generate, video_generate or preview - runs(id, graph_id, started_at, finished_at, state, estimated_cost, nodes_executed, nodes_from_cache) - jobs(id, run_id, node_id, input_hash, provider, model_path, request_id, state, submitted_at, completed_at, error) · request_id is written before the first poll - outputs(node_id, input_hash, kind, local_path, provider_url, bytes, created_at) · unique on node id and input hash, which is what makes a re-run free - assets(sha256, path, mime, bytes) · imported images and downloaded results, stored locally because remote URLs expire - The cache key is the node id plus a hash of its resolved inputs, so changing a prompt three nodes upstream re-executes everything downstream of it and nothing else. ### Phase 1 · A canvas that saves and cannot spend Build: Build the React Flow canvas with a palette of the six node types, wiring outputs to inputs, pan and zoom, and an autosave to SQLite on every change. Load models.json and render each model's ports and settings from it. Do not implement any provider call in this phase at all. Trap: Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. Done when: No module imported by the canvas can reach the provider client, enforced by a dependency assertion in the test suite. Editing, connecting and moving nodes autosaves and a reload restores the graph exactly, including node positions. Ports and settings for each model are rendered from models.json without model-specific code. The six node types are the only ones available. Do not build yet: No running, hashing, submission or downloads. ### Phase 2 · Running on purpose, and knowing the price first Build: Topologically sort the graph, resolve each node's inputs, hash them, and execute layer by layer. A node whose input hash already has an output is a cache hit and is not executed. Before any submission, show a plan: which nodes will execute, which are cached, and the estimated cost from per-model figures in models.json with the date you checked them. Trap: Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. Done when: Changing an upstream text node invalidates every downstream node and no others, tested on a branching graph. Re-running an unchanged graph executes nothing and reports every node as cached. A cycle in the graph is refused before anything runs, with the cycle shown. The run plan lists nodes to execute, cached nodes and an estimated cost, and pressing run without viewing the plan is not possible. Do not build yet: No provider submission, polling or downloads yet. ### Phase 3 · Jobs that survive the server restarting Build: Submit through the provider's queue endpoint and write the returned request identifier to the jobs table before the first status poll. Poll with backoff and record the state transitions the provider reports. On startup, find every job not in a terminal state and resume polling it. A duplicate submission for the same input hash is refused while a job for it is in flight. Trap: Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. Done when: Killing the server after submission and restarting resumes polling and completes the job, tested against a fake queue. A second run of the same unchanged node while a job is in flight joins the existing job rather than submitting again, tested by counting submissions. Every job row has its request identifier written before the first poll, asserted by ordering in the test. Polling backs off and a provider error is stored on the job with its message. Do not build yet: No downloads, spend ceilings or export yet. ### Phase 4 · Keeping what you paid for Build: Download every result to local storage as soon as a job completes, store it by content hash, and point the preview nodes at the local copy. The provider's documentation is explicit that result media URLs are subject to an expiry setting and that you should download what you want to keep (checked while writing this), so a run is not marked complete until its assets are on disk. Trap: Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. Done when: A completed job has its asset on local disk before the run is marked finished, asserted in the test. Previews render from local paths and never request the provider's domain, verified with the network monitored. The same output produced twice is stored once by hash. A missing local asset renders an explicit missing state naming the run and node rather than a broken image. Do not build yet: No spend ceiling, run history or graph export yet. ### Phase 5 · Ceilings, history and handing the graph to someone else Build: A spend ledger recording every job with its estimated and, where the provider reports it, actual cost. A daily and per-run ceiling that refuses to submit when crossed, with the figure shown on the run plan. A run history showing every node, its input hash, its job and its output. Export a graph with its models.json entries and its imported assets, and import it into an empty install. Trap: A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. Done when: A run that would cross the daily ceiling stops before the submission that crosses it and says which node it stopped at, tested with a low ceiling. In-flight committed cost counts towards the ceiling, tested by submitting concurrently. The run history shows every node with its input hash, job identifier and output, and links a broken preview to its run. Exporting a graph and importing it into an empty install reproduces the canvas, the model entries and the imported assets. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer on the canvas, which needs a conflict resolution model and a server you operate, and which is a large part of what the paid product sells. - A compositor and keyframe timeline, which is a separate application and nobody builds one in a weekend. - Credits and billing, since you hold the provider key directly and pay the provider's own prices rather than a marked-up balance. - Iterators and fan-out over arrays, which multiply cost in exactly the way the first rule exists to prevent; add them only after the ceiling has proved itself. - A second provider, because the queue semantics, model schemas and cost reporting differ enough that it is a rewrite of three phases rather than an adapter. ### README must contain - That generate nodes never execute on edit, only on the run buttons, and that a test asserts it by counting provider calls during editing - That every run spends real money at the provider, with the estimate shown on the plan and the figures in models.json carrying the date they were checked - That the request identifier is stored before the first poll so an in-flight job survives a restart - That result media are downloaded locally before a run is marked finished, because the provider's URLs expire - How to add a third model by editing models.json, with no code change
# Agent instructions · Wireflow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with React and React Flow in the same repository as the server, Express with better-sqlite3, no authentication, fal.ai through its queue API, one key in the environment, Declared in models.json with the path and which fields are ports versus settings, Node outputs keyed by node id plus a hash of resolved inputs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image.
# Build plan · Wireflow A local visual pipeline runner whose hard problems are never spending money by accident, surviving a restart with in-flight jobs intact, and keeping outputs after the provider's URLs have expired. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A canvas that saves and cannot spend The graph edits, autosaves and reloads, and no code path can call the provider yet. ### Steps 1. Build the canvas with autosave to SQLite Every change persists; none of them executes anything. Files: `src/canvas/Graph.tsx`, `server/db/schema.sql` 2. Render ports and settings from models.json Adding a model must never mean writing a component. Files: `models.json`, `src/canvas/NodeFromModel.tsx` 3. Assert the canvas cannot import the provider client A dependency test is the cheapest version of this rule. Files: `tests/no-provider-in-ui.test.ts` ### Done when - [ ] The canvas cannot import the provider client - [ ] A reload restores the graph including positions - [ ] Ports and settings come from models.json - [ ] Only the six node types exist ### Watch out - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. ## Phase 2 · Running on purpose, and knowing the price first Run executes a topologically sorted graph layer by layer, reusing cached outputs, after showing what it will cost. ### Steps 1. Hash resolved inputs, not node settings The difference between a correct cache and a confusing one. Files: `server/run/hash.ts`, `tests/cache-invalidation.test.ts` 2. Refuse a cyclic graph before executing A cycle plus a generate node is an unbounded bill. Files: `server/run/topo.ts` 3. Show the plan and the estimate before submitting The last moment where the run is still free. Files: `src/run/Plan.tsx`, `models.json` ### Done when - [ ] An upstream change invalidates exactly the nodes below it - [ ] An unchanged re-run executes nothing - [ ] A cycle is refused with the cycle shown - [ ] The plan and cost estimate precede every run ### Watch out - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. ## Phase 3 · Jobs that survive the server restarting A submitted job is recoverable from its identifier alone, from a cold start. ### Steps 1. Write the request identifier before polling The order is the whole guarantee. Files: `server/provider/submit.ts`, `tests/restart-recovery.test.ts` 2. Resume unfinished jobs at startup The table is the record; the poller is not. Files: `server/run/resume.ts` 3. Join an in-flight job rather than resubmitting Two clicks on run should cost what one costs. Files: `server/run/execute.ts` ### Done when - [ ] A restart mid-job resumes and completes it - [ ] A duplicate run joins the in-flight job - [ ] The identifier is written before the first poll - [ ] Provider errors are stored on the job ### Watch out - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. ## Phase 4 · Keeping what you paid for Every output lives on your disk before the run is called finished. ### Steps 1. Download before marking the run complete Complete should mean the bytes are yours. Files: `server/assets/download.ts`, `tests/assets.test.ts` 2. Store assets by content hash Identical outputs cost one copy. Files: `server/assets/store.ts` 3. Render previews from local paths only A preview that hits the provider is a preview that expires. Files: `src/nodes/Preview.tsx` ### Done when - [ ] Assets are on disk before a run is marked finished - [ ] Previews never request the provider domain - [ ] Duplicate outputs are stored once - [ ] A missing asset renders an explicit state ### Watch out - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ## Not in this build - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. ## After v1, if you want it - A third and fourth model added to models.json, which should need no code at all if Phase 1 was done properly - Fan-out over a list of prompts, but only once the ceiling has stopped a real run at least once - Seed pinning and a side-by-side comparison of two runs, which the input hash already makes possible
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The provider key. Server side only; the canvas never sees it. FAL_KEY=fal-...redacted # Required. Graphs, runs, jobs and outputs. DATABASE_PATH=./data/wireflow.db # Required. Imported images and downloaded results, stored by content hash. STORAGE_DIR=./storage # Required. Model paths, port definitions and prices with the date each was checked. MODELS_FILE=./models.json # Required. Checked before every submission, counting jobs already in flight. DAILY_SPEND_CEILING=5.00 # Required. Per run. Crossing it stops at the submission that would cross it. RUN_SPEND_CEILING=1.00 # Required. How long a job is polled before it is marked as needing manual checking. POLL_MAX_SECONDS=900
# Wireflow · product brief ## Problem Chaining a few model calls is a script, and ComfyUI already gives you a free node canvas. What you will not get in a weekend is the part after generation: a track and keyframe timeline with hosted rendering, and live multiplayer inside it. The graph is vibecodable. The editor attached to it is not. ## Product outcome A node canvas you can edit all day for free, that tells you what a run will cost before it starts, stops at the ceiling instead of after it, recovers jobs across a restart, and keeps every output on your own disk. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - fal.ai API key or another hosted model provider - Node 20 - somewhere to run it that stays up while jobs finish - a GPU only if you self-host the models instead ## Explicit non-goals for v1 - Multiplayer on the canvas. - A compositor or keyframe timeline. - Credits and billing. - Iterators and fan-out over arrays. - a compositor and keyframe timeline, with hosted rendering - live multiplayer on the canvas and inside the editor - hundreds of model schemas kept current for you - paid steps that never fire by accident, and jobs that survive a closed tab - a library of ready-made pipelines to fork, and boards you can share ## Success criteria - The canvas is confirmed unable to import the provider client - An upstream change is confirmed to invalidate exactly the nodes below it - A restart mid-job is confirmed to resume and complete it - Assets are confirmed on disk before a run is marked finished - A run is confirmed to stop at the submission that would cross the ceiling
# Build brief · Wireflow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local node canvas for chaining hosted media models, where nothing costs money until you press run to replace Wireflow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules, and the first one is the entire reason this build exists rather than a script. Generate nodes never run on edit. Not on a keystroke, not on a connection being made, not on a node being dropped on the canvas, not on the graph autosaving. Only the Run button and the per-node run button submit a job. Every reactive execution framework you might reach for will tempt you to wire generation into a state change, and the first time that fires in a loop it is a real invoice at a real provider. Put the rule in the README and assert it in a test that counts provider calls while the graph is edited. Second, persist the provider's request identifier before the first poll, not after it. A job submitted and then lost because the server restarted between submission and the first status check is a generation you paid for and cannot find, and the queue API exists precisely so that an identifier stored now can be queried later from a different process. ### Stack (fixed, do not substitute) - Frontend: Vite with React and React Flow in the same repository as the server · one repository, one command, no accounts, localhost only - Server: Express with better-sqlite3, no authentication · a single-user local tool has no business having a login - Provider: fal.ai through its queue API, one key in the environment · the queue gives you an identifier you can query from another process - Models: Declared in models.json with the path and which fields are ports versus settings · adding a model should be a data change, not a code change - Caching: Node outputs keyed by node id plus a hash of resolved inputs · a re-run should only pay for what actually changed ### Data model (create this before Phase 1) - graphs(id, name, nodes_json, edges_json, updated_at) · autosaved on every canvas change, and autosaving never triggers a run - nodes(graph_id, node_id, type, settings_json) · type is text, image_import, prompt_template, image_generate, video_generate or preview - runs(id, graph_id, started_at, finished_at, state, estimated_cost, nodes_executed, nodes_from_cache) - jobs(id, run_id, node_id, input_hash, provider, model_path, request_id, state, submitted_at, completed_at, error) · request_id is written before the first poll - outputs(node_id, input_hash, kind, local_path, provider_url, bytes, created_at) · unique on node id and input hash, which is what makes a re-run free - assets(sha256, path, mime, bytes) · imported images and downloaded results, stored locally because remote URLs expire - The cache key is the node id plus a hash of its resolved inputs, so changing a prompt three nodes upstream re-executes everything downstream of it and nothing else. ### Phase 1 · A canvas that saves and cannot spend Build: Build the React Flow canvas with a palette of the six node types, wiring outputs to inputs, pan and zoom, and an autosave to SQLite on every change. Load models.json and render each model's ports and settings from it. Do not implement any provider call in this phase at all. Trap: Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. Done when: No module imported by the canvas can reach the provider client, enforced by a dependency assertion in the test suite. Editing, connecting and moving nodes autosaves and a reload restores the graph exactly, including node positions. Ports and settings for each model are rendered from models.json without model-specific code. The six node types are the only ones available. Do not build yet: No running, hashing, submission or downloads. ### Phase 2 · Running on purpose, and knowing the price first Build: Topologically sort the graph, resolve each node's inputs, hash them, and execute layer by layer. A node whose input hash already has an output is a cache hit and is not executed. Before any submission, show a plan: which nodes will execute, which are cached, and the estimated cost from per-model figures in models.json with the date you checked them. Trap: Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. Done when: Changing an upstream text node invalidates every downstream node and no others, tested on a branching graph. Re-running an unchanged graph executes nothing and reports every node as cached. A cycle in the graph is refused before anything runs, with the cycle shown. The run plan lists nodes to execute, cached nodes and an estimated cost, and pressing run without viewing the plan is not possible. Do not build yet: No provider submission, polling or downloads yet. ### Phase 3 · Jobs that survive the server restarting Build: Submit through the provider's queue endpoint and write the returned request identifier to the jobs table before the first status poll. Poll with backoff and record the state transitions the provider reports. On startup, find every job not in a terminal state and resume polling it. A duplicate submission for the same input hash is refused while a job for it is in flight. Trap: Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. Done when: Killing the server after submission and restarting resumes polling and completes the job, tested against a fake queue. A second run of the same unchanged node while a job is in flight joins the existing job rather than submitting again, tested by counting submissions. Every job row has its request identifier written before the first poll, asserted by ordering in the test. Polling backs off and a provider error is stored on the job with its message. Do not build yet: No downloads, spend ceilings or export yet. ### Phase 4 · Keeping what you paid for Build: Download every result to local storage as soon as a job completes, store it by content hash, and point the preview nodes at the local copy. The provider's documentation is explicit that result media URLs are subject to an expiry setting and that you should download what you want to keep (checked while writing this), so a run is not marked complete until its assets are on disk. Trap: Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. Done when: A completed job has its asset on local disk before the run is marked finished, asserted in the test. Previews render from local paths and never request the provider's domain, verified with the network monitored. The same output produced twice is stored once by hash. A missing local asset renders an explicit missing state naming the run and node rather than a broken image. Do not build yet: No spend ceiling, run history or graph export yet. ### Phase 5 · Ceilings, history and handing the graph to someone else Build: A spend ledger recording every job with its estimated and, where the provider reports it, actual cost. A daily and per-run ceiling that refuses to submit when crossed, with the figure shown on the run plan. A run history showing every node, its input hash, its job and its output. Export a graph with its models.json entries and its imported assets, and import it into an empty install. Trap: A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway. Done when: A run that would cross the daily ceiling stops before the submission that crosses it and says which node it stopped at, tested with a low ceiling. In-flight committed cost counts towards the ceiling, tested by submitting concurrently. The run history shows every node with its input hash, job identifier and output, and links a broken preview to its run. Exporting a graph and importing it into an empty install reproduces the canvas, the model entries and the imported assets. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer on the canvas, which needs a conflict resolution model and a server you operate, and which is a large part of what the paid product sells. - A compositor and keyframe timeline, which is a separate application and nobody builds one in a weekend. - Credits and billing, since you hold the provider key directly and pay the provider's own prices rather than a marked-up balance. - Iterators and fan-out over arrays, which multiply cost in exactly the way the first rule exists to prevent; add them only after the ceiling has proved itself. - A second provider, because the queue semantics, model schemas and cost reporting differ enough that it is a rewrite of three phases rather than an adapter. ### README must contain - That generate nodes never execute on edit, only on the run buttons, and that a test asserts it by counting provider calls during editing - That every run spends real money at the provider, with the estimate shown on the plan and the figures in models.json carrying the date they were checked - That the request identifier is stored before the first poll so an in-flight job survives a restart - That result media are downloaded locally before a run is marked finished, because the provider's URLs expire - How to add a third model by editing models.json, with no code change
# Architecture · Wireflow ## Stack | Part | Choice | Why | | --- | --- | --- | | Frontend | Vite with React and React Flow in the same repository as the server | one repository, one command, no accounts, localhost only | | Server | Express with better-sqlite3, no authentication | a single-user local tool has no business having a login | | Provider | fal.ai through its queue API, one key in the environment | the queue gives you an identifier you can query from another process | | Models | Declared in models.json with the path and which fields are ports versus settings | adding a model should be a data change, not a code change | | Caching | Node outputs keyed by node id plus a hash of resolved inputs | a re-run should only pay for what actually changed | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Canvas | Editing, autosave and rendering nodes from models.json. | Any canvas library, provided it cannot reach the provider client. | | Runner | Topological order, input hashing and cache hits. | Any executor, provided hashing covers resolved inputs. | | Provider | Queue submission, identifier persistence and polling. | Any provider with a queue, provided the identifier is stored before the first poll. | | Assets | Downloads, content-addressed storage and previews. | Any store, provided a run is not complete until the bytes are local. | | Cost | Estimates, the ledger and the ceilings. | Not swappable for a per-run check, which fan-out defeats. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `FAL_KEY` · required, secret · The provider key. Server side only; the canvas never sees it. - `DATABASE_PATH` · required · Graphs, runs, jobs and outputs. - `STORAGE_DIR` · required · Imported images and downloaded results, stored by content hash. - `MODELS_FILE` · required · Model paths, port definitions and prices with the date each was checked. - `DAILY_SPEND_CEILING` · required · Checked before every submission, counting jobs already in flight. - `RUN_SPEND_CEILING` · required · Per run. Crossing it stops at the submission that would cross it. - `POLL_MAX_SECONDS` · required · How long a job is polled before it is marked as needing manual checking. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Wireflow product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite with React and React Flow in the same repository as the server, Express with better-sqlite3, no authentication, fal.ai through its queue API, one key in the environment, Declared in models.json with the path and which fields are ports versus settings, Node outputs keyed by node id plus a hash of resolved inputs. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. - A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway.
# Delivery milestones · Wireflow Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A canvas that saves and cannot spend The graph edits, autosaves and reloads, and no code path can call the provider yet. ### Steps 1. Build the canvas with autosave to SQLite Every change persists; none of them executes anything. Files: `src/canvas/Graph.tsx`, `server/db/schema.sql` 2. Render ports and settings from models.json Adding a model must never mean writing a component. Files: `models.json`, `src/canvas/NodeFromModel.tsx` 3. Assert the canvas cannot import the provider client A dependency test is the cheapest version of this rule. Files: `tests/no-provider-in-ui.test.ts` ### Done when - [ ] The canvas cannot import the provider client - [ ] A reload restores the graph including positions - [ ] Ports and settings come from models.json - [ ] Only the six node types exist ### Watch out - Reaching for a reactive graph library that recomputes downstream nodes when an input changes is the natural choice here and it is the one that empties your account, because a generate node is just another computation to it. Keep execution out of the component tree entirely: the canvas edits data, the server executes, and only an explicit request crosses between them. ## M2 · Running on purpose, and knowing the price first Run executes a topologically sorted graph layer by layer, reusing cached outputs, after showing what it will cost. ### Steps 1. Hash resolved inputs, not node settings The difference between a correct cache and a confusing one. Files: `server/run/hash.ts`, `tests/cache-invalidation.test.ts` 2. Refuse a cyclic graph before executing A cycle plus a generate node is an unbounded bill. Files: `server/run/topo.ts` 3. Show the plan and the estimate before submitting The last moment where the run is still free. Files: `src/run/Plan.tsx`, `models.json` ### Done when - [ ] An upstream change invalidates exactly the nodes below it - [ ] An unchanged re-run executes nothing - [ ] A cycle is refused with the cycle shown - [ ] The plan and cost estimate precede every run ### Watch out - Hashing only a node's own settings means changing a prompt template three nodes upstream leaves every downstream generate node believing it is cached, and you spend an evening wondering why the video still shows the old prompt. Hash the resolved inputs, which include everything upstream, so a change anywhere invalidates exactly the nodes below it and nothing more. ## M3 · Jobs that survive the server restarting A submitted job is recoverable from its identifier alone, from a cold start. ### Steps 1. Write the request identifier before polling The order is the whole guarantee. Files: `server/provider/submit.ts`, `tests/restart-recovery.test.ts` 2. Resume unfinished jobs at startup The table is the record; the poller is not. Files: `server/run/resume.ts` 3. Join an in-flight job rather than resubmitting Two clicks on run should cost what one costs. Files: `server/run/execute.ts` ### Done when - [ ] A restart mid-job resumes and completes it - [ ] A duplicate run joins the in-flight job - [ ] The identifier is written before the first poll - [ ] Provider errors are stored on the job ### Watch out - Polling in a loop that holds the request identifier only in memory means a restart during a two minute video generation orphans a job you have already paid for, and the only way to find it is the provider's dashboard. Write the identifier first, resume from the table on boot, and treat the in-memory poller as a convenience rather than the record. ## M4 · Keeping what you paid for Every output lives on your disk before the run is called finished. ### Steps 1. Download before marking the run complete Complete should mean the bytes are yours. Files: `server/assets/download.ts`, `tests/assets.test.ts` 2. Store assets by content hash Identical outputs cost one copy. Files: `server/assets/store.ts` 3. Render previews from local paths only A preview that hits the provider is a preview that expires. Files: `src/nodes/Preview.tsx` ### Done when - [ ] Assets are on disk before a run is marked finished - [ ] Previews never request the provider domain - [ ] Duplicate outputs are stored once - [ ] A missing asset renders an explicit state ### Watch out - Storing the provider's URL as the output and rendering previews from it makes a graph look fine for a few days and then quietly fill with broken images, and the run that produced them has already been paid for. Download on completion, store by hash, keep the provider URL only as a note about where it came from, and have the preview fall back to a clear missing-asset state rather than a broken image. ## M5 · Ceilings, history and handing the graph to someone else (production only) Spending is bounded and visible, and a graph plus its models is portable. ### Steps 1. Check the ceiling before every submission Once per run is not a ceiling, it is a greeting. Files: `server/cost/ceiling.ts`, `tests/ceiling.test.ts` 2. Record estimated and reported cost per job The gap between the two is what you learn from. Files: `server/cost/ledger.ts` 3. Export the graph with its models and assets A graph without its model entries is not reproducible. Files: `server/export.ts`, `tests/graph-roundtrip.test.ts` ### Done when - [ ] A run stops at the submission that would cross the ceiling - [ ] In-flight cost counts towards the ceiling - [ ] Run history links nodes, hashes, jobs and outputs - [ ] A graph export reimports into an empty install ### Watch out - A ceiling checked once at the start of a run does not stop a graph that fans out into forty video generations, because the check passed when the total was zero. Check the ceiling before every submission, count the committed cost of jobs already in flight, and refuse the next submission rather than cancelling the run halfway.
# Operations · Wireflow ## Backup The storage directory is the expensive part, since every file in it was paid for and cannot be regenerated at the same seed and price. Back it up more carefully than the database, which mainly holds graphs you could redraw. ## Restore Restore both, then mark any job not in a terminal state as needing manual checking rather than resubmitting it, and look those identifiers up at the provider before spending anything again. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Spend today against the ceiling, jobs polled past the maximum, outputs whose local asset is missing, and the age of the prices in models.json. A price checked eight months ago makes every estimate decorative. ## Incident checklist A surprise bill means either a ceiling was checked once per run or something executed on edit, and the jobs table with its submitted timestamps shows which. A missing output means a download failed after completion, and the request identifier is still in the jobs table to fetch it again while the provider's copy lasts. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The canvas is confirmed unable to import the provider client - [ ] An upstream change is confirmed to invalidate exactly the nodes below it - [ ] A restart mid-job is confirmed to resume and complete it - [ ] Assets are confirmed on disk before a run is marked finished - [ ] A run is confirmed to stop at the submission that would cross the ceiling ## Launch constraint Do not market omitted Wireflow capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The provider key. Server side only; the canvas never sees it. FAL_KEY=fal-...redacted # Required. Graphs, runs, jobs and outputs. DATABASE_PATH=./data/wireflow.db # Required. Imported images and downloaded results, stored by content hash. STORAGE_DIR=./storage # Required. Model paths, port definitions and prices with the date each was checked. MODELS_FILE=./models.json # Required. Checked before every submission, counting jobs already in flight. DAILY_SPEND_CEILING=5.00 # Required. Per run. Crossing it stops at the submission that would cross it. RUN_SPEND_CEILING=1.00 # Required. How long a job is polled before it is marked as needing manual checking. POLL_MAX_SECONDS=900
$ choose a build depth, inspect the files, then open the complete pack in your agent
Nobody vibecodes a keyframe timeline in a weekend. And the DIY failure mode is not that it breaks, it is that it keeps running while double-charging a call that 404'd or losing a render you already paid for.
xa compositor and keyframe timeline, with hosted rendering
xlive multiplayer on the canvas and inside the editor
xhundreds of model schemas kept current for you
xpaid steps that never fire by accident, and jobs that survive a closed tab
xa library of ready-made pipelines to fork, and boards you can share
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Wireflow →· no votes, no pay-to-list · just what's real
Wireflow pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | Up to 5 workflows; the first generation after setup is free, then generations consume paid credits. |
| starter | $24 | — | 1,600 credits per month; approximately 53 images or 94 video seconds; unlimited workflows. |
| professional | $45 | — | 4,300 credits per month; approximately 143 images or 252 video seconds; unlimited workflows. |
| team | $249/workspace | — | 4 seats and 21,500 credits per month; approximately 716 images or 1,264 video seconds; unlimited workflows. |
| enterprise | custom | — | Custom seats, credits and support; 0 self-serve prices published. |
free tierUp to 5 workflows; 1 first generation after setup is free, with no recurring included-credit allowance published.
billingmonthly only, no annual plan
hidden costsStarter credits expire at each billing-period end; Professional and Team credits roll over only 3 months; credit top-ups cost $10, and extra Team-seat pricing is not published.
verified 2026-08-14 · source ↗
Vibecode Wireflow
Kinda. The core of Wireflow is buildable in a weekend with the prompt on this page, but there are real gaps: a compositor and keyframe timeline, with hosted rendering, live multiplayer on the canvas and inside the editor. Read the honest list above before committing.
How much does Wireflow cost?
Wireflow costs about $24/month (Starter, checked 2026-07-31), which is $288 per year.
What do I lose by replacing Wireflow?
Honestly: a compositor and keyframe timeline, with hosted rendering; live multiplayer on the canvas and inside the editor; hundreds of model schemas kept current for you; paid steps that never fire by accident, and jobs that survive a closed tab; a library of ready-made pipelines to fork, and boards you can share. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Wireflow?
Yes: ComfyUI (The node canvas under half the AI-media industry, minus the credits and plus the cable spaghetti.) SwarmUI (A reusable image-video-audio workflow graph with a simpler Generate tab for days when the wires can wait.) NodeTool (A creative node canvas for image, video and audio with a timeline; 450 stars, but active releases and real installers.) All 3 curated free alternatives are at vibecodeit.com/wireflow/alternatives. The prompt is for when you want it exactly your way.