Skip to navigation

BFF Pattern

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

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:

{
"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:

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:

{
"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:

ClaimMeaning
subCessy user id. Becomes the runtime user principal.
scopeMust include runtime.
tenant_idMust match the {tenantId} path segment.
app_idsWhen present, must include the {appId} path segment.
client_idOAuth client that received the grant.
audOptional 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:

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:

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

SymptomLikely cause
401 Missing or invalid Authorization headerThe BFF did not forward Authorization: Bearer <token>.
401 Invalid or expired OAuth JWTThe access token expired or was signed for another Cessy issuer. Refresh the token.
403 Missing required scope: runtimeThe OAuth client requested the wrong scope. Re-authorize with openid runtime.
403 Token does not belong to this tenantThe token tenant_id does not match the path tenant.
403 Token does not have access to this appThe token app_ids claim does not include the path app.
403 User-delegated channel APIs require a user principalThe 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