Rishav Raj

Kureita, a node based AI video studio

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.

What
Personal project, live at kureita.com

Worked on


The idea

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.

The canvas and the runner

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.

What the user wires text imageGen videoGen script audioGen editorAgent export How the runner resolves it text, script, imageGen, audioGen, videoGen, editorAgent nodes with no unmet inputs run first, then the graph is walked forward
Each node writes its output into the run state, and downstream nodes read from there.

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.

Long jobs on short lived Lambdas

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.

POST /workflows/{id}/run API Lambda writes a run record returns a run id, in under a second self invoke, async POST /execute-background shared secret, fresh 15 min budget Runs nodes in order writes state as it goes MongoDB run state UI polls for progress
Locally there is no Lambda to invoke, so the same path falls back to a background task in process.

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.

Webhooks you can trust

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.

fal finishes a job Verify the ED25519 signature JWKS cached 24h, 5 min timestamp leeway 401, dropped already finished? yes ignore the duplicate no Download the result and re upload it to our own S3 Mark the node complete and charge the billable units
The signature and duplicate checks both run before anything that costs money.

Assets that do not expire

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.

Paying for every node

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.

Picking the right model

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.

Deployment

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.

Browser Next.js on Amplify FastAPI on Lambda Docker image via Lambda Web Adapter MongoDB Atlas workflows, runs, billing S3 assets and uploads AI providers fal, Runware, ElevenLabs
Traffic is bursty with long quiet gaps, so paying per request works out well here. Provider costs dominate anyway.

What I learned


Back to home · Knowledge assistant · Flexday RBAC · Fastail