Architecture#
BPMN Server has three layers: a host that people and other systems talk to, an engine that runs processes, and a core of model and data handling that both of them use. The host and the engine can share a process or run apart, and the application's code always stays with the engine.
The workflow pages and security are one part of their own (the dashed box): sign-in, users and groups, and the pages people work in. bpmn-web has a set, a skeleton to start from; your own application can be that part instead, with the same server behind it, or inside it (a user manager: Your application with the server inside). The model editor is a separate application, for designers.
flowchart LR
subgraph Internet["Internet"]
People["Users<br/>browsers, mobile apps"]
Others["Applications<br/>bpmn-client, bpmn-mcp"]
end
subgraph UIZone["Workflow pages and security: bpmn-web's, or your application's"]
Sec["Sign-in, users, groups, sessions"]
Pages["My work, task forms,<br/>start forms, instance page"]
Sec --> Pages
end
subgraph Core["BPMN Server: secure"]
subgraph HostZone["API host"]
REST["REST APIs"]
Admin["Administration"]
API["BPMNAPI<br/>each call as a user"]
REST --> API
Admin --> API
end
subgraph EngineZone["Engine: Secure and Isolated for tenants <br/>(ENGINE_AT, ENGINE_KEY)"]
Engine["Engine<br/>executions, timers, decisions"]
Code["Your code<br/>delegate, services, model scripts"]
Engine --> Code
end
end
subgraph DataZone["Data"]
Store[("Workflow store")]
Models["Models folder"]
end
subgraph EditorZone["Model editor: a separate application, for designers"]
Editor["Editor, tests, history"]
end
People --> Sec
Others -- "API keys" --> REST
Pages == "as the signed-in user" ==> API
API == "writes" ==> Engine
API -. "reads" .-> Store
Engine --> Store
Engine --> Models
Editor -- "tested models, through Git" --> Models
classDef outside fill:#f1f5f9,stroke:#64748b,stroke-width:1px
classDef replace fill:#e0f2fe,stroke:#0369a1,stroke-width:2px,stroke-dasharray:6 4
classDef private fill:#fef9c3,stroke:#a16207,stroke-width:2px
classDef inner fill:#fffbeb,stroke:#a16207,stroke-width:1px
classDef data fill:#ecfdf5,stroke:#047857,stroke-width:1px
classDef design fill:#f5f3ff,stroke:#6d28d9,stroke-width:1px
class Internet outside
class UIZone replace
class Core private
class HostZone,EngineZone inner
class DataZone data
class EditorZone design
bpmn-web's pages call BPMNAPI in the host's process; your application's pages call the REST APIs. Either way, each
call acts as the signed-in user. In development the host and the engine are one process, with the same calls between
them. What changes when your application replaces the box: From the skeleton to your application.
Who does what#
| Part | Does | Doesn't |
|---|---|---|
Host (bpmn-web) |
Serves the pages, the model editor and the REST APIs. Signs users in. Reads and saves models, instances and tasks. | Run a process, a timer, a decision or a test. It asks the engine. |
| Engine | Runs processes, timers, decisions and tests. Holds your delegate and services. | Face users. Nothing but its host should reach it. |
Core (bpmn-server) |
Reads models, checks them, and stores data, for both of the above. | Depend on the web application. |
Calls between the host and the engine carry JSON, in the same process or between processes. So code behaves the same in both setups, and a test that passes against one is testing the same calls as the other.
Isolation#
Each zone of the diagram trusts the one inside it, and only that one:
| Boundary | What crosses it | What guards it |
|---|---|---|
| Internet → host | Pages, the model editor, REST calls | Sign-in and sessions (bpmn-web's, or your application's through its user manager), CSRF, API keys and tokens, rate limits; every call then acts as a user, with that user's rights |
| Host → engine | Writes: starting, completing, assigning, tests | A private address (ENGINE_AT) and a key (ENGINE_KEY); nothing else reaches the engine |
| Engine → your systems | What your services and connectors call | Your code, and the connectors' keys, which only the engine holds |
The engine runs your code: delegates, services, and the scripts in models. Kept in a process of its own, a script that loops or a service that crashes stops the engine, not the pages. When it restarts, the waiting tasks and the timers are in the store as they were; a step it was in the middle of is recovered from Administration (see Hung tasks). The host reads the store and the models folder directly, to show them, and writes only through the engine.
The engine can also run as a user of its own, or in a container of its own, with only its private address open: what the separation makes possible, not what a default install does.
Why the API is on the host#
The REST APIs are a public door, so they are where the other doors are guarded: sign-in, keys, rate limits, in one place. An API call that starts or completes something is passed to the engine, and the host returns the result.
The engine stays private because it runs code a model brings with it. Put the API in the engine, and that process would face the internet. And the public address would be the engine's: restarting it, or moving it to another machine, would change it for every client. With the API on the host, the engine can be restarted, moved or replaced behind it.
An application that embeds the engine (BPMNServer.create) has no HTTP API at all: it calls BPMNAPI in its own process.
The UI and the API#
The host has three doors, and they meet at one API:
| Door | Used by | Reaches the engine through |
|---|---|---|
| Pages: My work, task forms, the instance page | Browsers | API v3's operations (My work, starting a process), else BPMNAPI; as the signed-in user |
The model editor's routes (/model/...) |
The model editor in the browser | The core, for models, checks and history; BPMNAPI for runs and tests |
REST: /api/v3 |
Other applications, your pages, AI agents | BPMNAPI, as the user a token (or your application's own credential) stands for, or as the service account (API_KEY) |
REST: /api, /api2, /api2/tasks (frozen) |
Older applications | BPMNAPI, as the key's service account or as the acting user |
The pages are becoming clients of API v3 like any other: My work and starting a process use its operations, in the
host's process, as the person API v3 would take; the other pages still call BPMNAPI directly. One contract to secure and to test,
and, once every page is on it, a UI that can run apart from the host. API v3 has the contract.
From the skeleton to your application#
bpmn-web is where an application starts, not necessarily where it ends. What your application does with each part:
| Part of bpmn-web | In your application |
|---|---|
| Sign-in, users and groups, the setup page | Replaced by your own: your sign-in, your user directory, your roles. The engine needs only each call's user and groups (IUserInfo); bpmn-web takes them from your user manager when it runs inside your application |
| My work, task forms, the start form, the instance page | Replaced by your pages, which call the REST APIs (or BPMNAPI, embedded). bpmn-web's are working examples to copy from |
The engine, BPMNAPI, your delegate and services |
Kept: this is what runs your processes |
| REST APIs | Kept, behind your application, on a private network |
| Administration | Kept, for your administrators, on the private network |
| Model editor, tests, Design dashboard | Kept as a stand-alone application for designers: a design server, whose tested models reach production through Git |
Your application in control#
Your application is the dashed box: it signs its users in and serves its own pages, and the rest of the diagram runs
on a private network behind it, with design off (DISABLE_DESIGN=true): the REST APIs, Administration and the engine.
In detail:
flowchart LR
subgraph Internet["Internet: your users"]
People["Browsers, mobile apps"]
end
subgraph AppZone["Your application: the public door"]
AppGuard["Your sign-in, your users,<br/>your security"]
AppPages["Your pages and forms<br/>task lists, requests"]
AppGuard --> AppPages
end
subgraph Private["Private network: only your application and your staff reach it"]
subgraph HostZone["bpmn-web in production: REST APIs and Administration (DISABLE_DESIGN=true)"]
REST["REST APIs<br/>/api (API_KEY): start for a user<br/>/api2/tasks (API_ACTING_USER_KEY): tasks"]
Admin["Administration<br/>for your administrators"]
API["BPMNAPI<br/>each call as a user"]
REST --> API
Admin --> API
end
subgraph EngineZone["Engine process (ENGINE_AT, ENGINE_KEY)"]
Engine["Engine<br/>executions, timers, decisions"]
Code["Your code<br/>delegate, services, model scripts"]
Engine --> Code
end
end
subgraph DesignZone["Design server: bpmn-web, stand-alone, for designers"]
Editor["Model editor, tests,<br/>Design dashboard"]
end
subgraph DataZone["Data"]
Store[("Workflow store")]
Models["Models folder"]
AppDb[("Your database<br/>your users, your records")]
end
Git["Git<br/>models, tests, services"]
People --> AppGuard
AppPages == "server to server: start, list tasks,<br/>complete as a user, deliver messages" ==> REST
AppGuard --> AppDb
API == "writes" ==> Engine
API -. "reads" .-> Store
Engine --> Store
Engine --> Models
Code --> AppDb
Editor -- "tested models" --> Git
Git -- "released" --> Models
classDef outside fill:#f1f5f9,stroke:#64748b,stroke-width:1px
classDef data fill:#ecfdf5,stroke:#047857,stroke-width:1px
classDef private fill:#fef9c3,stroke:#a16207,stroke-width:2px
classDef public fill:#e0f2fe,stroke:#0369a1,stroke-width:2px
classDef inner fill:#fffbeb,stroke:#a16207,stroke-width:1px
classDef design fill:#f5f3ff,stroke:#6d28d9,stroke-width:1px
class Internet outside
class AppZone public
class Private private
class HostZone,EngineZone inner
class DataZone data
class DesignZone design
- BPMN Server signs nobody in here: it trusts the user your application names, because your application holds its keys. Keep the keys on your application's server, and bpmn-web where only your application and your staff reach it.
- Your application starts a process for one of its users, lists, claims and completes their tasks with
/api2/tasksandAPI_ACTING_USER_KEY: the user is the initiator, and each task comes with its form./api2would start it as its service account. Integrate an existing application has the steps. - Designers work on a design server of their own. A model reaches production as a file, tested, through Git: see Release.
Your application with the server inside#
Your application and bpmn-web are one server: bpmn-web runs in your application's process, with your routes and pages
mounted in it, and your application signs people in. bpmn-web keeps no users, sessions or credentials of its own:
it takes the person from your application's user manager. Your pages show the workflow pages (My work, the
Workbench, the model editor, Administration) in your own layout, and your models call your application's API through a
connector. apps/hrdemo is this setup: an HR application with the server in it.
flowchart LR
subgraph Internet["Internet: your users"]
People["Browsers, phones"]
end
subgraph AppProcess["One server: your application, with bpmn-web in it"]
subgraph AppZone["Your application"]
UM["Your user manager<br/>sign-in, users, roles, sessions"]
AppPages["Your pages, your layout<br/>the workflow pages inside it,<br/>a To do panel"]
AppApi["Your REST API<br/>your records"]
end
subgraph HostZone["bpmn-web"]
WfPages["Workflow pages<br/>My work, task forms, Workbench,<br/>model editor, Administration"]
V3["API v3<br/>as the person your user manager names"]
API["BPMNAPI<br/>each call as a user"]
WfPages --> V3
V3 --> API
end
subgraph EngineZone["Engine"]
Engine["Engine<br/>executions, timers, decisions"]
Code["Your code, connectors"]
Engine --> Code
end
end
subgraph DataZone["Data"]
AppDb[("Your database<br/>users, sessions, records")]
Store[("Workflow store")]
Models["Models folder"]
end
People --> UM
UM -- "req.user, its session" --> WfPages
UM -- "apiUser" --> V3
AppPages -- "tasks, as the person" --> V3
UM --> AppDb
API == "writes" ==> Engine
Engine --> Store
Engine --> Models
Code -- "connector, its own key" --> AppApi
classDef outside fill:#f1f5f9,stroke:#64748b,stroke-width:1px
classDef data fill:#ecfdf5,stroke:#047857,stroke-width:1px
classDef public fill:#e0f2fe,stroke:#0369a1,stroke-width:2px
classDef inner fill:#fffbeb,stroke:#a16207,stroke-width:1px
class Internet outside
class AppProcess,AppZone public
class HostZone,EngineZone inner
class DataZone data
Who signs people in: your user manager. bpmn-web asks one thing of your application, an IUserManager given to
start({ userManager }):
| It does | When |
|---|---|
init(app): your sessions, and on each request req.user (the person: userName, userGroups) |
First, before bpmn-web's middleware |
setup(app): /login and /logout, your own pages or sending people to them |
Before bpmn-web's routes |
apiUser(req) (optional): who a call to API v3 is from, by your own credential (your session, your token) |
Each call to /api/v3 |
- Your roles become groups. The groups
req.usercarries are what the models name: a lane, a task's candidate groups,ADMINandDESIGNERfor administering and designing. The HR demo's roles giveEmployee,Manager,HRandADMIN. - Your application issues the credentials. API v3 takes the person your
apiUsernames; bpmn-web issues no tokens and no links for AI agents (POST /api/v3/tokensand/ai/keyrefuse). The service key (API_KEY) stays, for server-to-server calls. Signing out of your application signs out of the workflow pages too: they have no session of their own. - bpmn-web manages no users. No setup page at the first start, no sign-up; Administration and
/api/v3/userssay your application manages them. bpmn-web's own sign-in is its default user manager, unchanged. - The engine is unchanged. It needs only each call's
IUserInfo: task assignment is by name and group, checked against that person. Finding people (a group's members, an email) is your application's, in its delegate.
Your pages and the workflow pages, one site. Your routes are mounted in bpmn-web's server (start({ routes })),
before its own, so your pages, your API and the workflow pages share one address and one sign-in:
- The workflow pages open inside your layout, in a frame of the same site: framed so, a page leaves out its own menu
bar (the browser says it is framed, on HTTPS and localhost; its body has the class
framed). The HR demo's sidebar has a Workflow section, and/hr/workflow/<page>shows bpmn-web's<page>. - Your pages call API v3 with the person's session: the HR demo's To do panel lists their tasks (assigned to them, available to them, recently done) on every page, and opens a task's form.
- Planned: a task can name one of your forms instead of its fields (
camunda:formKey="app:<name>"): see the design notedocs/design/form-keys.md.
Your models call your application. A connector, made from your API's OpenAPI document, is how a model reaches your
records: the HR demo's hr connector (Assign Employee reads an employee and creates an assignment). It calls with a
key of its own (HR_SERVICE_KEY, kept encrypted with CONNECTORS_KEY); your API takes that key or a signed-in
person, and nothing else.
The design: docs/design/host-users.md.
Embedded#
Your application holds the engine in its own process, with no HTTP between them: your pages call BPMNAPI.
flowchart LR
subgraph Internet["Internet: your users"]
People["Browsers, mobile apps"]
end
subgraph AppZone["Your application's process: the public door, and the engine in it"]
AppGuard["Your sign-in, your users,<br/>your security"]
AppPages["Your pages and forms"]
API["BPMNAPI<br/>each call as a user (SecureUser)"]
Engine["Engine<br/>executions, timers, decisions"]
Code["Your code<br/>delegate, services, model scripts"]
AppGuard --> AppPages
AppPages -- "in-process calls" --> API
API --> Engine
Engine --> Code
end
subgraph DataZone["Data"]
Store[("Workflow store<br/>MongoDB, SQLite or memory")]
Models["Models folder"]
AppDb[("Your database")]
end
People --> AppGuard
API -. "reads" .-> Store
Engine --> Store
Engine --> Models
Code --> AppDb
AppGuard --> AppDb
classDef outside fill:#f1f5f9,stroke:#64748b,stroke-width:1px
classDef data fill:#ecfdf5,stroke:#047857,stroke-width:1px
classDef public fill:#e0f2fe,stroke:#0369a1,stroke-width:2px
class Internet outside
class AppZone public
class DataZone data
The simplest, and the least isolated: a model's scripts run in your web application's process. See Embedding the engine.
The four, side by side#
| Public door | Who signs users in | Workflow pages | Model code runs | |
|---|---|---|---|---|
| bpmn-web's pages and security | bpmn-web | bpmn-web | bpmn-web's | In the engine's process, private |
| Your application with the server inside | Your application, bpmn-web in its server | Your application (its user manager) | bpmn-web's in your layout, and yours | In the engine's process |
| Your application in control | Your application | Your application | Yours, through the REST APIs | In the engine's process, private, behind bpmn-web |
| Embedded | Your application | Your application | Yours, through BPMNAPI |
In your application's process |
Ways to run it#
| Setup | Host | Address | Engine | Set with |
|---|---|---|---|---|
| Development and test | 1 | 1 | 1, in the host's process | Nothing: this is the default |
| Production | 1 | 1 | 1, in its own process | ENGINE_AT, ENGINE_KEY |
| Design only | 1 | 1 | None | ENGINE_AT=none |
Installation has the settings for each one.
Your application can also embed the engine with no host at all, using new BPMNServer(...) or BPMNServer.create(...): see Application Integration.
Users#
Only signed-in users use a server. There is no guest access. Each person has an account, and their groups decide what they can do: see Security. The accounts are bpmn-web's own (its default user manager), or your application's when it signs people in (Your application with the server inside).
Tests#
Every test runs in the engine, including one started from the model editor. The host passes the test to the engine and shows the results, so a test runs your real delegate and services the way a real process does.
Tests are Gherkin .feature files beside their models. A scenario's last run is its test instance in the database, tagged with its process and scenario. See Testing and debugging models.
Where things are kept#
| What | Where |
|---|---|
| Models, decisions, their tests and version history | Files in the models folder (DEFINITIONS_PATH) |
| Connectors | Files in the models folder (<id>.connector.json); their keys, encrypted, in the workflow store |
| Instances, tasks, locks and test runs | The workflow store |
| Users and sessions | bpmn-web's: the workflow store, beside the instances (MongoDB, SQLite or memory). Your application's, with its user manager: your database |
| Your business records | Your own database. A process keeps an identifier and calls your services |
Packages#
| Package | Is |
|---|---|
bpmn-server |
The engine and core |
bpmn-web |
The host |
bpmn-client |
A REST client, for another Node.js application |
bpmn-mcp |
Connects AI clients to a running server over MCP. npx bpmn-server init installs it |