Architecture¶
Workflow is a modular monolith: one Go binary that runs in three modes, and one PostgreSQL database that holds all state, including the job queue. Only hooks faces the internet; the UI and API can stay on an internal network.
flowchart TB
users[People<br/>browser, API, CLI, AI assistants]
git[GitHub, GitLab<br/>webhooks]
scim[SCIM IdPs<br/>Okta, Entra ID]
internal[Internal ingress<br/>UI, /api, /auth, SSE]
public[Public ingress<br/>/hooks, /scim only]
subgraph app[One image, three modes]
web[web<br/>UI, REST API, MCP, SSE]
hooks[hooks<br/>verify and store webhooks, SCIM]
worker[worker<br/>jobs, automation, sync, email]
end
pool[PgBouncer<br/>CloudNativePG Pooler, optional]
pg[(PostgreSQL 17<br/>CloudNativePG)]
s3[(Object storage<br/>optional)]
apis[Outbound<br/>GitHub, GitLab, Google, Keycloak, SMTP, webhooks, chat]
users --> internal --> web
git --> public
scim --> public
public --> hooks
web --> pool
hooks --> pool
worker --> pool
pool --> pg
web --> s3
worker --> apis
Sign-in goes through the browser's redirect to the identity provider, so it needs no inbound path; Google and Keycloak directory sync are outbound calls from the worker.
Run modes¶
| Mode | Serves | Scales on |
|---|---|---|
web |
The UI, the REST API (/api/v1), sign-in (/auth/*), the MCP server (/mcp), live updates over server-sent events |
CPU (HPA) |
hooks |
Git webhooks (/hooks/{provider}/{connection}), the SCIM 2.0 server (/scim/v2), email unsubscribe links |
CPU; kept small and separate so a webhook storm cannot starve people |
worker |
Background jobs on River: webhook processing, automation, notifications and email, directory sync, workflow migrations, Jira imports | Queue depth (KEDA) |
all |
Everything, for development and small installs | — |
Why one database¶
PostgreSQL covers everything Workflow needs:
- JSONB for custom fields, validated against field definitions
- Full-text search and
pg_trgmbehind the query language - River, a Postgres-backed job queue: jobs are inserted in the same transaction as the change that caused them (the outbox pattern), so automation and notifications run if and only if the change committed
- LISTEN/NOTIFY to push changes to open browsers; behind a transaction-mode pooler each web pod keeps one direct session for it, and River polls instead of listening
Read more¶
- Internals: request path, domain, events and jobs, in detail
- Domain model
- Workflow engine
- Tech stack