The seven repositories
Seven repositories make up ModuleX, but only five of them run as processes — two are libraries and one is a static marketing site.
The backend serves on
http://localhost:8000 in development; that is the canonical base URL for every example in these docs. (A python -m app.main shortcut binds 8001 to avoid local conflicts, but the standard uvicorn command and all tooling use 8000.) The SDKs default to the production REST host https://api.modulex.dev.
The integration count is 175 live integrations — the figure to use anywhere a tool count appears. A vendored
dist/manifests.json snapshot in modulex-integrations lags at 136 entries; do not quote it. See the integration catalog for the authoritative list.The three shared backplanes
Every component above is wired together by exactly three external services.Managed datastore
The authoritative datastore. The backend is the only authoritative writer;
modulex-ws does its own reads and writes against the same storage. The Procrastinate job queue lives in a dedicated procrastinate schema in the same datastore.In-memory store
Used by the backend for run-event pub/sub, history replay, rate-limit counters, caches, and credit reservation; used by
modulex-ws as the Socket.io adapter and for presence and lock keys.Clerk
The external identity provider. It issues the user JWTs that the backend, the app, and the realtime server all verify against Clerk’s JWKS.
- The backend never reads or writes the
presence:*orlock:workflow:*keys. Those belong exclusively tomodulex-ws. - Beyond those keys, the two backends share only a narrow pub/sub surface; external workflow sync runs over the Socket.io
workflow:external-syncevent.
Component and data-flow map
The five deployables
There are five things that actually run. The other repositories are libraries (modulex-integrations, modulex-js, modulex-python) or a static site (modulex-www).
Boot order
The two long-lived servers each have a deterministic startup sequence.- API server (modulex)
- Realtime server (modulex-ws)
1
Configure logging
The root log level comes from
LOG_LEVEL (default INFO).2
Validate security settings
In
production or staging, a failed security check aborts startup. In development it logs a warning and continues.3
Run the startup sequence
Applies migrations and syncs the integration catalog from the installed
modulex-integrations package into the database.4
Connect the in-memory store
Falls back to a local in-process implementation if the store is unreachable.
5
Start the event worker and pub/sub subscriber
Wires the run-event delivery machinery (multi-instance aware).
6
Initialize the checkpointer and pre-warm the DB pool
The state checkpointer is always the managed saver.
7
Register routers
All 21 REST routers mount at their resource prefixes. There is no
/v1 segment.The REST surface
The backend exposes 21 routers, mounted directly at their resource prefixes (/workflows, /composer, /assistant, /knowledge-bases, /credentials, and so on). There is no API version segment — no /v1 or /api/v1. Trailing-slash redirects are disabled, so a path is matched exactly as written.
Every response body is snake_case JSON (Pydantic v2 default); request headers are PascalCase-hyphen (X-Organization-ID, X-API-KEY). The full request lifecycle, content types, and base URLs are covered in the API overview; the environments and versioning policy live in Base URLs, environments & versioning.
Standard error envelopes
The backend emits three error-envelope shapes, and you must branch on all of them. Each is owned by a different surface:
The complete shape catalog, status mapping, and which surface emits each lives in Errors & status codes.
Auth and org context across every hop
The same two pieces of context — a Clerk JWT (or anmx_live_ API key) and an organization id — thread through every wire, but the transport differs per hop.
The org context header is
X-Organization-ID (exact casing) on every REST and SSE hop. The realtime server resolves org membership independently, through its own cached SQL lookup. The full model — Clerk JWT versus API key, roles, and the org header — is in Auth model: JWT vs API key and Org context & X-Organization-ID.
Live org roles are owner and admin only — the member role is retired. Composer, Assistant, and managed-knowledge actions require owner or admin. See Roles & permissions.
Two realtime planes
ModuleX has two realtime systems that do not share a transport.Run-event streaming
Owned by the backend, delivered over SSE on top of server-side pub/sub. Carries workflow, Composer, and Assistant run progress:
metadata, node_update, tool_call, interrupt, done, and more. Run endpoints emit flat data: {"type": …} frames; the discriminator is the type key inside the JSON, with no event: line.Canvas collaboration
Owned by
modulex-ws, delivered over Socket.io. Carries presence, cursors, node locks, node and edge edits, and external sync. The handshake takes the Clerk JWT and the org id; events are node:add, patch, lock, and the like.workflow:run:{run_id}:events receives events published by any other replica. The realtime server scales horizontally through the Socket.io store adapter. The end-to-end model is in Realtime overview & event taxonomy and the realtime & collaboration model.
Not all SSE in the system uses the same frame convention. Run and agent streams use flat typed-JSON
data.type frames; the sidebar chat-list stream (GET /chats/stream) uses named SSE events (event: chat_list_updated). A consumer that crosses both must expect both conventions.The tool runtime seam
The most architecturally significant internal seam is how the backend turns a catalog name such asgithub.create_issue into an executed, billed action against the installed integrations package.
1
Discover
At startup, the backend queries Python entry points in the
modulex.tools group from the installed modulex-integrations package and caches the result for the process lifetime. Restart the worker to pick up a freshly installed package version.2
Sync
The startup sequence syncs the catalog into the database. The runtime then reads auth schemas from the database catalog, not from live package manifests — so a new manifest field is inert until the catalog is re-synced against the bumped package.
3
Load
The backend imports each tool’s
@tool-decorated function and wraps it so that credential fields are stripped before the model ever sees them.4
Execute
The executor resolves and decrypts the credential (refreshing OAuth2 tokens that are close to expiry), injects auth into the call, and invokes the tool. Tools run only inside workflow runs and Composer or Assistant turns — there is no REST endpoint for direct tool execution.
5
Bill
Managed (
modulexai) usage is metered in credits; bring-your-own-key (BYOK) credentials are tracked but not credit-limited.modulex-integrations package, but not all: legacy JSON-defined integrations such as mcp_server (which loads tools dynamically from a custom MCP server credential) are also loaded, and the package wins on a name collision. The full tool contract is in Build an integration.
SDK client configuration
Both SDKs wrap the same REST surface and send the same headers. The constructor shapes differ in one structural way: the Python client reads environment-variable fallbacks; the JavaScript client does not.string
required
An
mx_live_… API key. In Python this falls back to the MODULEX_API_KEY environment variable; in JavaScript it must be passed to the constructor or the client throws.string
The organization id sent as
X-Organization-ID. In Python it falls back to MODULEX_ORGANIZATION_ID; in JavaScript it is constructor-only.string
default:"https://api.modulex.dev"
The REST host. In Python it falls back to
MODULEX_BASE_URL; in JavaScript it is constructor-only.number
default:"30000 ms (JS) / 30.0 s (Python)"
Per-request timeout. No environment fallback in either SDK.
number
default:"3"
Retry budget for retryable requests. No environment fallback in either SDK.
workflowId (a deployed workflow) or an inline workflow definition, plus input, config, and stream.
run_id, created_at). The Python SDK is snake_case in both directions. The full route-to-method mapping, naming differences, and parity gaps are in the SDK ⇄ API parity matrix.
Realtime & SDK notes
A couple of notes on the realtime and SDK surfaces.External sync uses Socket.io
External sync uses Socket.io
Live external canvas sync — changes from the REST API, the Composer, or a deployment reaching open canvases — flows over the Socket.io
workflow:external-sync event. See Socket.io collaboration events and Realtime co-editing & external sync.The subscriptions SDK resource is Python-only
The subscriptions SDK resource is Python-only
The
subscriptions resource exists only in the Python SDK; the JavaScript SDK has no equivalent. See Subscriptions & Stripe and the parity matrix.How repositories stay in contract
The repositories deploy independently, so ModuleX coordinates breaking changes through a file-based “brief” protocol: the backend holds inbound brief directories (one per sibling repository), each a markdown file with an acknowledgment block. Two conventions hold across them:- Breaking changes merge in the order backend → realtime server → app, with each side holding its branch until the upstream field lands.
- Any new integration manifest field (a new env var or OAuth setting) is inert until the catalog sync re-reads the bumped package into the database — the database catalog, not live manifests, is the runtime source.
Where to go next
How ModuleX works
The end-to-end mental model, from a prompt or canvas to a running, observable workflow.
Realtime overview
The two realtime planes and their event taxonomies in full.
API overview
The request lifecycle, content types, and how every operation is shown three ways.
Base URLs & environments
Hosts, environments, and the no-
/v1 versioning policy.