> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.cessy.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.cessy.ai/_mcp/server.

# BFF Pattern

> Use a Backend-for-Frontend with Cessy custom pages when UI orchestration belongs outside the event-sourced model.

A Backend-for-Frontend can help a custom page compose data and actions without leaking credentials or overloading the event model.

## Use a BFF for

* combining Cessy projections with third-party read data
* server-side calls that require secrets
* UI-specific aggregation that should not become a domain projection
* file transforms, exports, or imports that need controlled server logic
* feature flags or workspace-specific preview helpers
* user-delegated chat flows that need to combine Cessy channels with frontend session state

## Do not use a BFF for

* replacing commands with arbitrary mutations
* bypassing permissions
* storing business facts outside the event history
* hiding long-running policy behavior from Activity
* making design-time API calls from runtime pages

## Recommended shape

Keep the BFF thin:

1. authenticate the user
2. authorize against app intent
3. call Cessy Runtime APIs with a user-delegated token
4. call approved external services
5. return UI-shaped data

If the BFF starts owning decisions, move that behavior into commands, events, and policies.

## Partner chat flow

For an external partner frontend, keep chat orchestration in the BFF and keep channel state in Cessy:

1. list safe app agent metadata with `GET /api/rest/<tenant-id>/<app-id>/agents`
2. create a fresh conversation with `POST /api/rest/<tenant-id>/<app-id>/channels/dm`
3. list visible threads with `GET /api/rest/<tenant-id>/<app-id>/user-channel-views`
4. load messages with `GET /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/messages`
5. post user messages with `POST /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/messages`
6. advance read state with `POST /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/read`

The agent catalog returns only `id`, `name`, `label`, and `kind`. Use a published
agent's selected `id` as `agentId` in the DM request. A Workspace catalog can
also contain preview-only agents, but DM creation resolves its target from the
published app configuration; promote those agents before opening a DM. Each
successful DM request creates a new conversation and returns its `channelId`
and `sessionId`; repeating the request does not resolve an older conversation.
Continue an existing thread by posting to its known channel id. The BFF may
shape these responses for the UI, but it should not invent channel ids, mutate
business state directly, or store authoritative conversation history outside
Cessy.

When the user sends a natural chat message from a concrete product context,
include `agentTurnContext` in the DM/open or message request instead of
prefixing hidden instructions into `content`. This keeps the channel message
public and human-readable while giving the agent private, turn-scoped
orientation:

```json
{
  "content": "Can you review this?",
  "agentTurnContext": {
    "schemaVersion": "partner.buddy.turn-context.v1",
    "scope": "next_message_only",
    "surface": "buddy_rail",
    "focus": {
      "kind": "decision",
      "id": "decision-1",
      "truthStatus": "draft"
    },
    "domainPointers": [
      {
        "projection": "DecisionVersionHistoryView",
        "id": "version-1",
        "purpose": "current_decision"
      }
    ],
    "permissions": {
      "mayWriteDocument": false,
      "writeRequiresExplicitUserAction": true
    },
    "extensions": {
      "partnerBuddy": {
        "activeTab": "decision"
      }
    }
  }
}
```

Use the context for ids, projection pointers, truth status, compact snapshots,
orientation flags, and small namespaced `extensions` for builder-specific
hints. Do not put secrets, long transcripts, or large documents there. The
context does not authorize actions; Cessy Runtime permissions and explicit
user-action requirements still apply.

Custom Page Chat and legacy workspace sends that still include `pageContext`
without `agentTurnContext` are normalized into the same private, turn-scoped
context internally. Do not rely on page context appearing in public messages or
hidden routing text.

For `POST /channels/dm`, include `agentTurnContext` only when the same request
also posts `content`; `createOnly` DM opens do not have an associated turn.

## Cessy Auth for BFFs

Use delegated user auth. The BFF should obtain a Cessy OAuth access token for
the logged-in human user, then call Runtime REST with that token:

```http
Authorization: Bearer <cessy-oauth-access-token>
```

Do not use a tenant API key for user-facing BFF actions. API keys are for
server-to-server automation and authenticate as `apiKey` principals.
User-delegated surfaces, including partner chat and channel-read endpoints,
require a `user` principal and reject API keys and agent service tokens.
The read-only `GET /agents` catalog can use normal Runtime REST authentication,
but that does not make the same API key valid for a subsequent channel call.

### OAuth flow

1. Register an OAuth client for the partner BFF with `/api/oauth/register`. Use
   the BFF callback URL as the redirect URI and request `openid runtime`.
2. Start an authorization-code + PKCE flow at `/api/oauth/authorize` with
   `scope=openid runtime` and the target `tenantId` and `appId`. If the client
   supports a resource parameter, use the app Runtime MCP resource as the OAuth
   resource: `https://<cessy-origin>/api/mcp/<tenant-id>/<app-id>`.
3. Cessy shows its consent screen. The user must be signed in through Cessy Auth
   and must have access to the selected tenant; Cessy also validates that the
   selected app belongs to that tenant.
4. Exchange the authorization code at `/api/oauth/token` with the PKCE verifier.
5. Store the refresh token server-side in the BFF. Access tokens expire after 1
   hour and can be refreshed with the refresh-token grant.

Token responses look like this:

```json
{
  "access_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<refresh-token>",
  "scope": "openid runtime"
}
```

The access token is a Cessy-issued JWT. Runtime REST uses these claims:

| Claim       | Meaning                                                                |
| ----------- | ---------------------------------------------------------------------- |
| `sub`       | Cessy user id. Becomes the runtime `user` principal.                   |
| `scope`     | Must include `runtime`.                                                |
| `tenant_id` | Must match the `{tenantId}` path segment.                              |
| `app_ids`   | When present, must include the `{appId}` path segment.                 |
| `client_id` | OAuth client that received the grant.                                  |
| `aud`       | Optional resource audience when the OAuth request included `resource`. |

Runtime REST then resolves the user's principal context and applies the app's
command, projection, page, and channel authorization rules. The BFF should treat
Cessy as the authority for tenant/app membership and app permissions; local BFF
checks should only narrow access for UI intent, never broaden it.

### Calling Runtime REST

For app data, use the generated app contract:

```http
GET https://<cessy-origin>/api/rest/<tenant-id>/<app-id>/openapi
Authorization: Bearer <cessy-oauth-access-token>
```

Then call the generated command and projection endpoints described by that
OpenAPI document. For partner chat or channel UIs, the app-scoped Runtime REST
channel endpoints use the same delegated user token:

```http
GET https://<cessy-origin>/api/rest/<tenant-id>/<app-id>/agents
Authorization: Bearer <cessy-oauth-access-token>

GET https://<cessy-origin>/api/rest/<tenant-id>/<app-id>/channels?view=summary
Authorization: Bearer <cessy-oauth-access-token>
```

Channel endpoints additionally require that the channel contains an agent from
`{appId}` and that the delegated user can read or act on the channel.

### Partner setup checklist

When onboarding a partner BFF, configure the integration with tenant-specific
values supplied through that tenant's Cessy setup:

* one OAuth client per partner BFF callback
* Cessy users granted access to the tenant
* authorization requests scoped to the tenant and app
* BFF calls to `/api/rest/{tenantId}/{appId}/...` with the delegated user token

Do not place real tenant ids, app ids, client secrets, refresh tokens, or user
ids in docs, prompts, screenshots, page bundles, or shared configuration.
Keep those values in environment-specific secret storage.

### Common auth failures

| Symptom                                                    | Likely cause                                                                        |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `401 Missing or invalid Authorization header`              | The BFF did not forward `Authorization: Bearer <token>`.                            |
| `401 Invalid or expired OAuth JWT`                         | The access token expired or was signed for another Cessy issuer. Refresh the token. |
| `403 Missing required scope: runtime`                      | The OAuth client requested the wrong scope. Re-authorize with `openid runtime`.     |
| `403 Token does not belong to this tenant`                 | The token `tenant_id` does not match the path tenant.                               |
| `403 Token does not have access to this app`               | The token `app_ids` claim does not include the path app.                            |
| `403 User-delegated channel APIs require a user principal` | The BFF used an API key or agent service token for a user-only endpoint.            |

## Verification

Test the Cessy runtime path and the BFF path separately. A custom page is not
verified until the command/projection contract works without the page and the
page works against the published contract.

For BFF auth, verify at least one delegated user flow end to end:

1. complete OAuth login and consent for the target tenant/app
2. fetch `/api/rest/<tenant-id>/<app-id>/openapi` with the issued token
3. call one generated read endpoint or channel summary endpoint through the BFF
4. confirm an API key fails on user-delegated channel endpoints