Gateway Model: Apps, Channels, Sessions
Applies to: ChengOS v0.1.0+ | Last verified: 2026-08-12 | Source:
chengflow-sdk/README.md,chengflow-sdk/src/sdk/types.ts
The gateway is the layer between external applications and ChengOS workflows. Its whole job is mapping: which app a message came from, which workflow should handle it, and which conversation it belongs to. Getting this model right is what keeps a multi-app deployment from becoming guesswork.
Note the naming: the gateway is called chengflow-app / cheng-app in deployment configs, and
its source lives in chengflow-sdk/.
Four identifiers
Everything rests on keeping these separate:
| Identifier | Answers | Notes |
|---|---|---|
workspaceId | Which tenant? | Always explicit, so a multi-workspace deployment cannot mis-route |
channelId | Where did this message come from? | A workspace-unique slug, e.g. weather-app. Not a workflow identifier. |
boundWorkflowId | What should run? | A workflow UUID, bound when the channel is configured |
sessionId | Which ongoing dialogue? | Maps to a durable conversation |
The rule that matters
channelId identifies origin. boundWorkflowId selects execution. They are never the same
thing, and the backend must never infer one from the other.
Earlier versions used channelId to look up the workflow, which conflated three separate
concerns: app-origin identity, workflow selection, and adapter-level authentication. The
consequence was that you could not point two apps at one workflow, or move an app to a different
workflow, without renaming things that other parts of the system depended on.
The current model binds the workflow explicitly at channel-configuration time. channelId then
does only what its name says: it tells the gateway which app to route the result back to.
Authentication
The browser gateway authenticates with a JWT and nothing else. It does not use adapter keys.
That is a deliberate boundary. chk_-prefixed channel keys exist for server-side channel
adapters — a Telegram or Slack integration calling in from outside — and they only work against
/api/v1/channel/*. A browser front end reusing an adapter key would be embedding a
long-lived shared secret in client-side code. See
authentication.
Message flow
External app
↓
Gateway accepts the connection and pairs the app
↓
Gateway resolves channelId → app mapping, and boundWorkflowId → execution target
↓
Gateway calls the backend with the user's JWT
↓
Backend executes the workflow
↓
Backend returns the result / pushes execution events
↓
Gateway routes the result back to the right app via channelId + session mapping
The division of labour: the gateway owns pairing, mapping, and routing; the backend owns execution. Neither guesses at the other's job.
Channel configuration
A channel record carries:
| Field | Meaning |
|---|---|
id | The record's own UUID (primary key) |
channelId | The business slug, unique within the workspace |
boundWorkflowId | The workflow UUID to execute |
name, description | Human-facing labels |
appType | Which kind of app |
connectionConfig | Per-adapter settings |
enabled | Whether it accepts traffic |
connectionState | Lifecycle state |
webhookUrl | Where external platforms deliver messages |
Note id and channelId are both present and are not interchangeable: id is the database key,
channelId is the slug you use in application code.
Manage channels under /api/v1/workspaces/:workspace_id/channels, and their keys under
/channel-keys.
Sessions and conversations
A session has an id (the session_id sent to the backend), a display label, a
createdAt, an optional conversationId, and a pinned flag. It also carries a derived
executionStatus — pending, running, waiting_for_review, completed_unread, or
failed_unread.
The *_unread states are a UI concern, not a backend one: they let the gateway show that a
session finished while the user was looking elsewhere. They are derived by the execution
coordinator and never persisted by the session manager, so treat them as view state.
Sessions map onto durable conversations. The conversation is the source of truth for history; the session is the client's handle on it.
Two REST calls per turn
The browser path is:
POST /api/v1/workspaces/:workspace_id/conversations/resolve— find or create the conversation.POST /api/v1/conversations/:id/messages— post the message and trigger execution.
The response gives conversation_id, workflow_id, and optionally execution_id. Subscribe to
that execution_id over WebSocket for streaming output.
Next steps
- SDK quickstart — this model in code.
- Authentication — JWTs, channel keys, and tickets.
- Publishing a workflow as an app — the alternatives.