# 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](/docs/api-authentication).

## Message flow

```text
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:

1. `POST /api/v1/workspaces/:workspace_id/conversations/resolve` — find or create the
   conversation.
2. `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](/docs/sdk-quickstart) — this model in code.
- [Authentication](/docs/api-authentication) — JWTs, channel keys, and tickets.
- [Publishing a workflow as an app](/docs/app-publish-workflow) — the alternatives.
