Skip to content

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_trgm behind 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