Architecture
The platform app, orchestrator API, NATS, Postgres, and runtime workloads.
The platform has a control plane (the Next.js platform app, the Go orchestrator, Postgres, NATS, and the observability service) and a data plane where every deployed integration runs as its own Kubernetes Deployment of the same octo-runtime image. This page maps the components and traces the main request paths between them.

Component map
you (browser)
│ HTTPS (the cluster's ingress controller)
▼
┌───────────────────────────────────────┐
│ Platform app (apps/platform) :3000 │ Next.js editor + BFF
│ Auth.js OIDC session · server proxy │
└──────────────┬────────────────────────┘
│ HTTP (server-side only)
▼
┌───────────────────────────────────────┐ ┌────────────────┐
│ Orchestrator (Go) :8090 │───────▶│ Kubernetes API │
│ integrations · deployments · KV │ applies└──────┬─────────┘
│ snapshots · secrets · users · keys │ manifests │ creates
└───────┬──────────────────────┬────────┘ ▼
│ SQL │ publishes ┌──────────────────────────┐
▼ ▼ │ Runtime pods (data plane)│
┌──────────────┐ ┌───────────┐ │ one Deployment per │
│ Postgres │ │ NATS │◀──────│ deployed integration │
│ :5432 │ │ :4222 │ queues│ (octo-runtime image) │
└──────────────┘ └─────┬─────┘ telem.└──────────────────────────┘
▲ │ internal.logs · internal.traces
│ SQL ▼
│ ┌──────────────┐
└──────────────│ Observability│
│ service │ :8091 (observability/)
└──────────────┘All control-plane components run in one namespace (octo-dev by default) and are wired by the Helm chart.
Platform app (apps/platform)
A Next.js App Router application that serves the visual editor and every platform view. It is also the backend-for-frontend: the browser never calls the orchestrator directly. Server actions and route handlers under app/api proxy every call using the server-only ORCHESTRATOR_URL, so the orchestrator needs no CORS and no public exposure. It does authenticate: every proxied call carries the caller's own platform token, and the orchestrator checks it and the roles on it before serving the route.
Authentication is Auth.js (NextAuth) with an OIDC provider and JWT sessions. There is no database adapter, so the same config backs both the route handlers and the request proxy (proxy.ts), which redirects unauthenticated browser navigations to sign-in and answers unauthenticated /api/* calls with a 401.
One application endpoint bypasses the session gate because it carries its own authentication: /mcp (bearer tokens). The welcome page, /api/auth* and two static images are exempt as well.
At sign-in the provider's token is traded with the iam service for a platform token carrying the caller's octo user id and their roles; /mcp trades its caller's bearer for one the same way. It rides on the encrypted session cookie and is deliberately kept off the session object the browser can read, since that object is served at /api/auth/session. The proxy is what renews it: auth() called from a server component discards the cookies a renewal produces, and only the proxy's wrapper writes them back. See Users and API Keys.
The platform app holds no per-run state, which is what lets it scale past one replica. Pressing Run in the editor asks the orchestrator for a dev run, a pod of its own with its own hostname, so a status call, a log stream, and a Stop all work whichever replica answers them. The MCP endpoint's run tools go to the same place, keyed the same way, so an agent and a human editing one integration share a run. The one-shot debug operations (invoking a single flow, evaluating a CEL expression, running test suites) still spawn a short-lived child of this pod from the YAML in the request and finish inside it.
Orchestrator (orchestrator/)
A Go HTTP API, listening on port 8090 by default (PORT). It owns all persistent state in Postgres and is the only component with Kubernetes API access. Its feature modules register REST routes on a single mux:
| Module | Routes | Requires |
|---|---|---|
| integration | POST/GET /integrations, GET/PUT/DELETE /integrations/{id} | database |
| folder | POST/GET /folders, GET/PUT/DELETE /folders/{id}, member routes | database |
| snapshot | POST/GET /integrations/{id}/snapshots, DELETE /snapshots/{id}, frozen-resource reads | database |
| resource | POST/GET /integrations/{id}/resources, GET/PUT/DELETE .../resources/{resourceId} | database |
| apikey | POST/GET /users/{userId}/apikeys, DELETE .../apikeys/{id}, POST /apikeys/verify | database |
| kv | GET/PUT/DELETE /deployments/{id}/kv/{namespace}/{key} + object browser routes | database |
| deployment | POST/GET /integrations/{id}/deployments, GET/PATCH/DELETE /deployments/{id}, rollout, pod logs | database + in-cluster Kubernetes |
| devrun | POST/GET /devruns, GET/DELETE /devruns/{id}, reload, logs, bundle, expire | database + in-cluster Kubernetes |
| secret | GET /secrets, PUT/DELETE /secrets/{name} | database + in-cluster Kubernetes |
GET/PUT /settings/email, POST /settings/email/test, POST /email/send | database | |
| llm | GET/PUT /settings/llm | database |
| openapi | GET /openapi.json, GET /openapi/operations | nothing: the description is embedded in the binary |
| agent | GET/DELETE /settings/agent, POST /settings/agent/{install,rollout,tracing,autofix,deployment,max-iterations} | database + in-cluster Kubernetes |
The wiring degrades gracefully. Without DATABASE_URL, only /healthz, the OpenAPI routes (which describe the build rather than what it is connected to) and a degraded /db-version serve. Outside a cluster the deployment and secret routes stay disabled while everything database-backed still works.
The orchestrator verifies the caller's platform token on every route but the health and description endpoints, and applies a role policy to it. The acting user's id is still forwarded in request bodies (actorId) for attribution, and is still not a credential — what authorizes the call is the token. Keep the orchestrator internal (ClusterIP) anyway; the chart does.
iam (iam/)
A small Go service (port 8093) that owns identity and authorization for the platform. It is the only component that talks to an identity provider: it verifies the token somebody arrives with, resolves them to a users row and its roles, and mints a token this platform signed. That token is verifiable against the keys iam publishes at /.well-known/jwks.json, so a service that checks it needs to understand nothing about OIDC. Both the platform app and the orchestrator verify tokens against those keys.
It signs ES256 and generates, stores and rotates its own keys — there is no signing key for an operator to set. The private halves live in iam_signing_keys encrypted under KV_ENCRYPTION_KEY, the same key the orchestrator uses for the KV store.
POST /auth is the exchange and POST /auth/refresh renews the result, re-reading roles from the database so a revocation lands within a token lifetime. It also owns the users table and the routes that administer it — those require a platform token carrying platform:admin, checked by iam itself and not only by the app in front of it.
ClusterIP only, and it publishes no OpenAPI: it is not a surface to expose or to hand to an agent as a tool.
The orchestrator's {userId} path segments are ids iam issued; it serves no /users collection of its own.
Postgres
The system of record, applied idempotently from sql/schema.sql by a schema Job on every install/upgrade. Main tables: integrations, integration_idx_structure + integration_folder_members (folders), integration_snapshots + integration_resource_snapshots (version tags), integration_resources (live env files/templates), integration_deployments, cluster_secrets (names only; values live in Kubernetes), kv_store, site_settings (one jsonb row per site-wide setting), users, api_keys, and logs.
NATS
Core NATS (no JetStream) is the message backbone between planes. It carries four kinds of traffic on disjoint subject spaces: deployment-scoped runtime queues and topics (octo.{deploymentId}.q.* / .t.*), log shipping (internal.logs), trace shipping (internal.traces), and control-plane events (internal.deployments.*, internal.integrations.events). See Event Bus.
Observability service
A small Go service (observability/, port 8091) that consumes internal.logs and internal.traces as a competing consumer (so replicas scale safely), persists log records to the logs table and trace records to traces/trace_summaries, and serves the query API behind the platform's logs, traces and metrics views. It also serves the pod stats the stats sidecar writes to Redis, the retention policy, and the storage report. See its API reference, Monitoring and Traces.
Runtime pods
Each deployed integration becomes one Kubernetes Deployment of the generic octo-runtime image with the frozen definition mounted from a ConfigMap. The orchestrator injects RUNTIME_SERVICES_MODULE=k8s plus the deployment id, its own URL, and the NATS URL, so the same image self-configures for clustered state, queues, leader election, and log shipping. See Deployments and Clustering.
Request paths
Saving an integration
- You edit a flow and save. The browser calls a server action on the platform app; the action requires an authenticated session (and a write role, when configured).
- The BFF calls
PUT /integrations/{id}on the orchestrator with the new name/definition and the acting user's id. - The orchestrator validates the name (unique, case-insensitive), writes the row to Postgres, stamps
updated_by, and returns the stored integration.
Deploying an integration
- The deploy dialog fetches
GET /integrations/{id}/deployments/optionsfor the chosen tag: declared env vars, which of them the tag's.envresources already satisfy, and a suggested free slug. - On confirm, the BFF calls
POST /integrations/{id}/deploymentswith the settings (snapshot id, replicas, slug, env bindings, exposure). - The orchestrator resolves the tag's frozen definition, verifies every required env var is provided, records the deployment row, then creates the cluster resources: a ConfigMap, a Deployment of
octo-runtime, and, for HTTP integrations, Services and optionally an Ingress. A Kubernetes failure rolls everything back. - Kubernetes informers watch the new pods. On each change the orchestrator recomputes the integration's deployment list and publishes it to NATS on
internal.deployments.{integrationId}; the BFF's SSE route relays it to the browser, which shows the pods going ready live.
Invoking a deployed integration
- From another integration in the cluster, call the deployment's stable internal Service,
http://octo-int-{slug}.{namespace}:{port}, load-balanced across the deployment's replicas. - From the public internet (externally exposed deployments), the request hits the cluster's ingress controller at
https://{subdomain}.{baseDomain}, matches the deployment's Ingress (TLS from cert-manager or a shared wildcard certificate), and is routed to the per-deployment Service and on to a runtime pod. - Inside the pod, the runtime's HTTP source handles the request exactly as it would standalone. The flow YAML is identical in both worlds.