Architecture
How Nodaro is built, for operators and contributors. The editor, the Fastify API, the BullMQ workers, Redis, storage, the three auth modes and the editions.
Nodaro is a REST-first AI workflow engine. A browser editor lets users connect AI nodes — image and video generation, video composition, text models and audio — into a graph. The graph is stored in Supabase Postgres, and when it runs, a Fastify API sorts it and hands each node's work to queue workers on Redis. Model providers do the generation, and the results land in S3-compatible storage.
The same code serves three editions, Community, Business and Cloud, and accepts three kinds of access tokens. This page is for operators who run Nodaro and for contributors who change it.
The system at a glance
The repository
The public repository, github.com/nodaroai/app.nodaro.ai, is one npm workspaces monorepo:
| Folder | What lives there |
|---|---|
backend/ | The Fastify API, the BullMQ workers and the orchestrator. Node.js 22, TypeScript. |
frontend/ | The browser app: the editor, the app runner and the admin panel. React 19, React Router 7, React Flow. |
packages/ | Shared code and the published npm packages @nodaro/shared, @nodaro/sdk and @nodaro/cli, and the Remotion video compositions. |
supabase/ | The database schema, as forward-only SQL migrations. |
docs/ | The product documentation, including design notes. |
tools/ | Operator scripts: the key generator, the contract probe, backup and restore. |
examples/ | Examples, such as a GitHub Actions workflow that updates a server over SSH. |
Frontend
The frontend is a Vite single-page app. In the Docker image, the web server, Caddy, serves it as static files. It holds four products in one bundle:
- The editor — the React Flow canvas, a settings panel per node family, and a graph executor that runs workflows in the browser during editing.
- The app runner — shows a published workflow as a simple form of inputs and results.
- The OAuth consent screen — where users approve third-party apps and MCP clients, at
/oauth/authorize. - The admin panel — on the Business and Cloud editions only.
Server state lives in React Query, interface state in Zustand, and canvas state in React Flow's own store.
API
The API is a Fastify server on Node.js 22, written in TypeScript. Routes are Fastify plugins, and every endpoint declares a schema for its request and response. The same schemas validate each request and produce the OpenAPI 3.1 document at /v1/openapi.json. GET /v1/nodes lists the node types that the install can run.
A single authentication hook runs before every route: see Authentication.
Processes and queues
The backend ships as five Node.js processes in one image:
| Process | Role |
|---|---|
| API server | The HTTP API. |
| Media worker | Takes per-node jobs from the queue, calls the model providers and uploads results to storage. More than 40 job types, such as image and video generation, ffmpeg processing and audio. Default concurrency: 50, because it mostly waits on the network. |
| Render worker | The Remotion renderer, for compositions, motion graphics, Lottie overlays, 3D titles and composites. It runs a headless Chrome and is limited by CPU. Default concurrency: 2. |
| Orchestrator | Runs whole workflows: reads a run, sorts its graph and runs it level by level. Default concurrency: 20. |
| Pipeline worker | The Story to Video pipeline. It ships in every edition, but runs only on Nodaro Cloud and exits at once elsewhere. |
In Docker, one start script launches them all, and Caddy fronts the API. In a scaled install, each can run in its own container. They coordinate only through the BullMQ queues in Redis and the run records in Postgres. See Scaling.
How a workflow runs
When a user clicks Run in the editor, there are two paths:
- In the browser. During editing, the editor runs the graph itself and calls one API endpoint per node. You see each result as soon as its node finishes.
- On the server. Triggered runs, from a webhook, a schedule or the API, app runs, and explicit server runs go to the orchestrator. It saves progress in the run's record, so any client can follow it.
The orchestrator:
- Loads the workflow's nodes and connections.
- Sorts the graph into levels. One level holds the nodes whose inputs are all ready.
- Runs each level's nodes in parallel, up to the user's own parallelism and the server's ceiling,
MAX_CONCURRENT_NODES_PER_EXECUTION. - Runs each node in one of three ways:
| Way | Where | Examples | Why |
|---|---|---|---|
| Queued | A job for the media or render worker | Generate Image, Generate Video, Text to Speech, Combine Videos, Render Video | Long work or external calls, decoupled from the orchestrator for back-pressure |
| Direct HTTP | An internal call to an API route | Prompt, Motion Graphics, the prompt helper, the social publishing nodes | Quick text model calls, with no queue overhead |
| Inline | Inside the orchestrator | Combine Text, Split Text, Composite | Pure logic with no external calls |
- After each node, passes its output to the nodes that depend on it, and saves the run's progress with one database write per node.
A run can stop in two ways. Cancelled stops at once: nodes already running are abandoned, and credits already charged are not rolled back. Stopping finishes the current level, then stops before the next one.
Limits. Each node may take up to 90 minutes, and a whole run up to 120 minutes. A node that declares its own time budget, today Apply EDL, sized from the edit it renders, gets that budget instead, and the run's limit grows by the same amount. If the orchestrator restarts during a run, a node with a budget whose worker is still alive is resumed instead of run twice. Sub-workflows run recursively, up to 5 levels deep, with cycle detection.
Storage and database
- Media — every generated image, video and audio file goes to S3-compatible storage and is referenced by its key. Nodaro never deletes stored media by itself.
- Database — Supabase Postgres holds profiles, projects, workflows, runs and their progress, jobs, media records, credit transactions, OAuth apps and their tokens, personal API tokens and encrypted social connections. Every table enforces row-level security, so the database itself keeps users to their own rows.
- Migrations — the schema lives in
supabase/migrations/, applied in filename order and only forward. - Redis — holds the queues and small shared caches. Its job state is short-lived: see Backups and restore.
Authentication
The API accepts three kinds of bearer tokens and tells them apart by their shape:
| Token | Comes from | Acts as |
|---|---|---|
eyJ…, a JWT | A Supabase sign-in session in the editor | The signed-in user |
ndr_app_<64 hex> | An OAuth app that a user authorized | The user who authorized the app, limited to the granted scopes |
ndr_<64 hex> | A personal API token | The token's owner |
The processes of one install also authenticate to each other with INTERNAL_ORCHESTRATOR_SECRET, compared in constant time. The web server strips that header from outside requests.
For each request, the API checks in this order:
- Public routes, such as
/v1/openapi.jsonand incoming webhooks, skip authentication. - The internal secret, for calls between the processes.
- An OAuth app token, which must not be revoked or expired.
- A Supabase session token. The user's role is read from their profile and cached for 5 minutes.
- Anything else answers
401.
Personal API tokens are checked by the routes that accept them, which also apply rate limits and limit a token to its workflows. See Authentication and OAuth apps.
Editions in the code
One codebase serves all three editions:
- Community (
EDITION=community, the default) — self-hosted, with no admin panel, no credit ledger and no billing. Anyone who signs up is a regular user. - Business (
EDITION=business) — adds the admin panel and user management, still self-hosted and without billing. - Cloud (
EDITION=cloud) — adds credits, billing and credit prices set by admins. It powers app.nodaro.ai and is not meant for self-hosting.
Routes check the edition's capabilities, not its name: an admin route runs only on an edition with an admin panel, and credit code runs only on an edition with credits. The check comes first in the route, before any other logic. The browser app reads its edition at build time, from VITE_EDITION. See Editions and surface profiles.
Status and streaming
Job and run status is polled, every 2 to 5 seconds: it changes on a scale of seconds, and polling is simple to deploy. Streaming output — text from the text models, workflow runs and pipelines — uses Server-Sent Events. That is why a reverse proxy in front of Nodaro must not buffer responses.
Why these choices
- Fastify — first-class TypeScript types, schema-driven validation, and plugins that keep each route in its own scope.
- BullMQ — a mature queue on Redis with retries, backoff and concurrency controls, in the same Node.js stack.
- REST — easy OAuth scoping per route, a generated OpenAPI document, and a simple model for third-party clients.
- Supabase — Postgres, sign-in, realtime and row-level security in one service. Row-level security replaces much custom authorization code.
- React Flow — a proven canvas for node graphs, which Nodaro extends heavily.
- Remotion — declarative React compositions that render the same on the server as in the editor's preview.
- One provider layer — every model call goes through one abstraction, so a model can move to another provider without changes to feature code.
Frequently asked questions
Related
Scaling
Contributing
Authentication
TypeScript SDK
Editions and surface profiles
Last updated on
Troubleshooting
Fix common problems of a self-hosted Nodaro, starting at /setup. Symptoms and fixes for startup errors, ports, CORS, migrations, storage, queues and keys.
Contributing
Contribute code to Nodaro on GitHub. Prepare the development environment, branch from dev, run the tests, add a changeset and sign the contributor agreement.