Making a short video with AI usually means using four or five services in sequence and moving files between them by hand. Kureita puts that whole sequence on one canvas.
You place nodes on a canvas and connect them. Image generation, video generation, voiceover, uploads, and an editor agent at the end that writes and runs TSX to build the export. The connections decide the execution order, so there is no separate config to keep in sync.
Next.js and React Flow on the front, FastAPI on Lambda behind it, around 15k lines of Python. I wrote most of it.
Nodes get their inputs from whatever is wired into them, so the runner does not need a separate execution plan. It builds an in degree map from the edges, runs whatever has no unmet inputs, then repeats. That is Kahn's algorithm and it comes to about forty lines.
A video node that needs a start frame from an earlier clip reads an extracted frame instead of the whole video. Those extractions are cached, otherwise ffmpeg would run again every time someone moves around the graph.
Video generation takes minutes. API Gateway times out at thirty seconds and Lambda caps at fifteen minutes, so a request that waits for the result is not going to work.
Instead the request writes a run record, returns a run id and finishes. The Lambda then invokes itself asynchronously against an internal endpoint guarded by a shared secret rather than a user token. The point of that second invocation is that it starts with a fresh fifteen minute budget and its own log stream. It writes progress to MongoDB as it goes, and the browser polls the run record.
I would not call this a clean design, but it is what the platform allows and it kept everything in one deployment. Adding a queue and a worker fleet felt like too much for a product that was still changing shape every week.
Generations that run on fal call back when they finish. That callback endpoint is public and it changes billing state, so it needed more care than the rest of the API.
Every callback is verified before anything else happens. fal signs with ED25519, so we fetch their JWKS, cache it for a day, and check the signature allowing five minutes of clock drift on the timestamp. A bad signature returns 401 and nothing downstream runs.
After that there is an idempotency check. Webhooks arrive more than once, so a job that already reached completed or failed is ignored rather than processed again. Without it a retry would charge the account a second time.
There are two expiry problems here, and neither throws an error when it goes wrong.
Provider CDN links do not last. When a job finishes we download the result and store it in our own bucket, so older videos still load.
Our own presigned URLs expire too, and a canvas sticks around for months. Stored S3 URLs get signed again whenever a workflow is read, with the old signature stripped off first, so the UI always gets a URL that currently works.
Every node run costs money at a provider, so an account holds a USD balance rather than a credit that has to be converted later.
Charges use the billable units the provider sends back, since that is the number they invoice on. A run that would push the balance below zero returns a 402 before any provider is called. Deposits arrive through payment webhooks, and there are voucher codes for trials.
Video providers accept different inputs. Some are text only, some need a start image, some take both a start and an end frame. Expecting users to keep track of that would have generated a lot of support questions.
So the node checks what is wired into it and ranks the available models by whether they can handle those inputs, rather than defaulting to one. Wire an image in and it picks an image to video model on its own.
The API is a Docker image on Lambda behind the Web Adapter, managed with SAM, which means it stays an ordinary FastAPI app rather than something built around a Lambda handler. The frontend is on Amplify and deploys on push. Uploads go straight to S3 through presigned URLs, so media never passes through the function.