The engine runs flows. It's a separate project and server (Script/engine, Bun) that owns real browsers: one Docker container per browser, each with Chrome driven by Patchright and a KasmVNC display for the live view. mooflair builds and seals the work; the engine executes it.
In this section
| Page | Covers |
|---|---|
| Browsers | The browser record, containers, reconcile, idle stop, capacity, profiles |
| Runs | Payloads, the queue, the walk, events and controls, called flows, error flows |
| Live view | Signed links, the KasmVNC proxy, view vs control |
| API reference | Every route, its auth, request and response |
| Configuration | Environment variables, Docker Compose, commands, building the browser image |
Architecture
flowchart LR
subgraph mooflair[mooflair · Next.js]
page[Studio page]
server[Server actions\nrun · token]
end
subgraph engine[Engine · Bun]
api[HTTP + WebSocket routes]
mongo[(MongoDB)]
nats{{NATS}}
end
subgraph browser[browser-<id> container]
agent[Agent · Patchright]
chrome[Chrome]
kasm[KasmVNC :6901]
end
server -- run, user token --> page
server -- start, stop, config --> api
api -- /api/internal: config, what a run needs, events --> server
page -- socket, live link --> api
api <--> mongo
api <--> nats
nats <--> agent
agent --> chrome
kasm --- chrome
api -- proxy --> kasm
| Part | Role |
|---|---|
| mooflair | Stores flows, campaigns, nodes, datasets (the config). Signs the user token, starts and stops apps, and answers the engine's internal API (an app's config, what a run needs). Never runs automation. |
Engine server (src/) | HTTP and WebSocket routes, MongoDB records (apps, browsers, runs, logs), the scheduler (campaign windows, restarts, Schedule triggers, break resumes), the run queue, Docker reconcile. |
| NATS | Message bus between the engine and each browser's agent (browser.<id>.*). |
Browser container (docker/patchright) | KasmVNC display + the agent (agent/patchright), running Chrome with a persistent profile. |
Who calls what
- mooflair server → engine: the shared secret (
Authorization: Bearer <ENGINE_SECRET>), "server" routes (apps, runs, browsers). - engine → mooflair: the same secret on
/api/internal/*(MOOFLAIR_URL): apps' config, what a run needs, a browser's first use, events, usage. - Studio page → engine: directly from the user's browser, with a short-lived user token (HS256 JWT
{ sub: userId, exp }signed with the same secret) and CORS limited toENGINE_CORS_ORIGIN. Runs, run sockets, live links, stats. - Engine ↔ agent: NATS only. Each browser gets its own Docker network shared with just the engine and NATS, so browsers can't reach each other.
Design rules
- Generic executor. The engine knows no node types. Node code arrives compiled in the app's definition (
module.exports = async (ctx) => ({ item })), kept by its hash until it changes. - Secrets never touch the database. A run's context (with its secrets) lives in memory only (
appRuns). An engine restart fails every run that was going (campaign runs resume by themselves). - One run per browser. A browser's runs queue behind each other.
Data kept
| Collection | Holds | Kept |
|---|---|---|
Browser | Identity per user and key, config, desired state, live link version | until deleted |
App | Each campaign's and flow's config (mooflair's) and state (windows, restarts, failures in a row, schedule) | while mooflair places it here |
Run | Status, node states, cursor, graph it ran, its effect nodes, AI totals | 90 days |
RunLog | Log lines | 30 days |
Browser profiles and downloads live on disk in library/browser/<id>/{profile,download}.
Projects layout
engine/
src/
index.ts # Bun.serve routes, NATS loops, idle stop, reconcile timer
routes/ # workspace/[id], campaign/[id], flow/[id], run, run/[id], log, app, browser/[id], file, metrics
controllers/
browser/open.ts # find or create a browser by user + key, mark running
browser/reconcile.ts # make Docker match MongoDB
browser/pull.ts # a mooflair Browser brought over on first use
app/index.ts # appLoad, appStart, appStop, appCheck (the scheduler), appEnd, appPatch, appDelete
lib/
app/ # what is running: apps, appBrowsers, appRuns, appCampaign
app/app/index.ts # App: a campaign's or a flow's browsers by name, turn queue, lease, dispose
app/browser/index.ts # AppBrowser: one named browser's container start
app/run/index.ts # AppRun: the walker, events, controls, viewers
flow/expression/ # {{ … }} templates
database/, docker/, nats/, authentication/
agent/patchright/ # the agent inside each container
docker/patchright/ # browser image: Dockerfile, entrypoint, kasmvnc.yaml, Chrome policy
docker/nats/
docker-compose.yml # engine, nats, mongo, browser-patchright (build)
Tests
npm run test:walker (in mooflair, with the engine and the dev server up) runs flows of pure nodes through the real engine, with no accounts and no replies, and checks: a call two levels deep, a failing called flow, the error flow, pause, resume and cancel inside a called flow, the turn queue, a priority run beside the main run, a jump from a loop item, the call cache, GET /app and clearing a campaign's runs. It deletes everything it made. Run it after changing the walker.
FAQ
Can mooflair talk to several engines?
Yes. Engines are registered in mooflair's admin (/admin/engine) and each user picks one (top-right engine picker).
What happens if the engine restarts mid-run?
Every run that was going fails with The engine restarted: the engine walks the runs in memory, and an agent stops its steps once the engine's lease has been silent for a minute. With MOOFLAIR_URL set the engine then tells mooflair, and every campaign run inside its window resumes at once from the node it was on (finished nodes keep their outputs). A run cut off in a step that acts on the outside world (a reply) isn't resumed; you get a notification to check it. Other runs: Retry from failed.