> 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.

# Build Custom Pages

> Build app-specific Cessy pages that use generated runtime contracts instead of duplicating business rules.

Custom pages are for app-specific user experiences. They should make Cessy workflows easier to operate, not create a second business system beside the model.

## Good page design

A good custom page:

* centers the user's business task
* reads from generated projections
* executes generated commands
* shows validation and command failures clearly
* avoids hidden policy or permission logic in the frontend
* keeps layout stable across preview and runtime contexts

## Shared Page resources

Page source can import shared modules, assets, and public Design System aliases.
Use `list_page_resources` to page through bounded metadata; source and asset
content are intentionally omitted from list results. Use `read_page_resource`
with an exact resource ref for deterministic chunks, follow `nextOffset` until
`hasMore` is false, and compare the returned SHA-256 when reconstructing a large
payload.

When `upsert_page`, `patch_page_source`, or `replace_page_source` references an
existing published resource that the Workspace has never owned, Cessy verifies
the stored artifact hash and pins only the referenced transitive resource
closure into the Workspace. The response lists those additions in
`materializedDependencyRefs`. They are ordinary Workspace changes and remain
visible in the ChangeSet; the platform does not copy the app's unrelated Page
resource catalog or revive a resource explicitly removed from the Workspace
base.

For transport-fragile large TSX files, call `stage_design_content` repeatedly
with chunks of at most 64,000 bytes. Keep `idempotencyKey`, the complete file's
`expectedSha256`, `totalChunks`, scope, purpose, media type, and encoding stable,
and increment the zero-based `chunkIndex`. Intermediate responses identify
missing chunks and do not return a usable `contentRef`; the final response does
so only after SHA-256 verification.

`upsert_page`, `patch_page_source`, and `replace_page_source` bundle before a
revision-fenced Workspace write. Their mutation responses expose a deterministic
operation id plus the candidate artifact hash and status. If an Apply response
is lost, query `inspect_page_source_mutation` with that operation id; an exact
retry is idempotent. Retryable database-capacity failures include the same lookup
and a retry-after hint. A legacy source already above the normal size limit can
be replaced from a staged ref only by a no-growth or smaller complete source.

## Tailwind styling

For vNext registry authoring, selecting a curated component through
`registrySelection` also supplies its Tailwind v4 entry when the tenant does
not have one. Component source and styling prerequisites are installed in one
atomic Workspace revision; authors do not run an installer or a build command.
The existing tenant Visual Base, styles and valid Tailwind entry are preserved.
If a prerequisite path is occupied or an explicit entry is incompatible, Apply
returns an actionable conflict instead of overwriting it. Old Tailwind
`base/components` directives and reference-only imports do not establish the
required v4 foundation. Existing CSS-only Pages are not automatically migrated
unless their Workspace explicitly selects registry components.

When the app Design System declares a `tailwind.entry`, write normal Tailwind
utilities in the Page source and its imported Page modules. Cessy scans only
that exact transitive Page graph and compiles a selector-scoped stylesheet for
Preview and Evaluation. Host chrome, sibling Pages, and portal content outside
the Page boundary do not inherit those utilities.

Finalize a fresh Candidate after changing the Page source, imported modules,
the Tailwind entry, or its theme inputs. The accepted Candidate retains the
exact generated CSS bytes together with their SHA-256, compiler version, entry
asset, source/dependency closure, cache identity, and style boundary. Promotion
publishes those retained bytes without running Tailwind again. Operate,
standalone, public, and deployment-preview routes select the Page module and
stylesheet from the same deployment; whole-Release restore switches that
coherent graph together.

Missing, stale, oversized, or digest-mismatched generated CSS blocks
publication or rendering instead of falling back to a mutable stylesheet. If
the diagnostic reports a missing utility or undiscoverable dynamic class,
replace runtime-built class fragments with complete statically discoverable
class names, narrow oversized imported source graphs where necessary, then run
a new Evaluation and finalize a new Candidate. Legacy Page rebuild paths cannot
publish a Tailwind-enabled Page because they have no accepted Evaluation bytes.

## Data access

Use generated runtime contracts for data:

* projections for lists, details, dashboards, and operational views
* commands for business actions
* GraphQL when the page benefits from flexible UI-shaped queries
* a BFF only when page orchestration genuinely belongs outside the event model

Do not query design-time surfaces from a runtime page.

## Session and sign-out controls

Use `useSession()` from `@ces/app-sdk` for a page-owned sign-out button.
Standalone pages do not render Operate chrome, so they should expose an
accessible button whenever the authenticated experience needs a way out. Keep
the button disabled while `state === 'pending'` and render `error` in a
`role="alert"` region.

The host owns sign-out mechanics. It validates the app and physical calling
surface, clears the identity session, selected tenant, impersonation, and stale
synthetic Page-session state, and redirects only to `/auth/login`. Page source
must not call a private auth route, send user or tenant selectors, guess cookie
names, choose a redirect, or persist session state.

`useSession().supported` is true only for physical authenticated Operate and
standalone sessions. Public shares, Workspace previews, browser Evaluation,
Agent Page Sessions, and unauthenticated renders fail safely as typed
unsupported surfaces without a request or navigation.

## Calling external services for the current principal

Call external services through named adapter operations, not direct browser
fetches. When a downstream must know the authenticated Page principal, enable
the operation's small signed-assertion contract:

```json
{
  "exposure": {
    "customPage": {
      "enabled": true,
      "authRules": ["'knowledge-user' in $principal.roles"],
      "principalAssertion": {
        "audience": "knowledge-adapter",
        "credentialName": "KNOWLEDGE_PRINCIPAL_ASSERTION_SECRET",
        "keyId": "current"
      }
    }
  }
}
```

Page code continues to use `useAdapterOperation` or
`useCesContext().callAdapterOperation`. Cessy authenticates the Page principal,
evaluates exposure and `authRules`, then injects a fresh server-owned
`X-CES-Principal-Assertion`. The fixed token is an HS256 JWT with
`typ: ces-principal-assertion+jwt`, a 60-second lifetime, and an 8-KiB limit.
The Page cannot read, supply, or override the assertion or its signing secret.
Do not send `principalId`, roles, groups, or tags from Page state and expect the
downstream to trust them.

The downstream must fail closed. It verifies the exact algorithm, type, key ID,
signature, issuer, audience, times, token ID, and signed tenant/app/runtime
source/adapter/operation context before using `sub`. Missing, duplicated,
malformed, expired, oversized, or wrongly scoped assertions deny the request;
browser identity fields are never a fallback. HS256 makes the verifier a
co-signer, so use at least 32 random secret bytes and a distinct key for every
adapter trust domain/audience and environment.

Adapter `authRules` use OR semantics. If several constraints must all hold,
combine them in one expression. Two entries for role and account status would
grant role **or** status rather than requiring both.

### Effective access pull pattern

Assertions intentionally carry identity, not app-specific permissions. Model
fine-grained knowledge access in an `EffectiveKnowledgeAccess` projection keyed
by principal ID:

```json
{
  "id": "<principal-id>",
  "active": true,
  "allowedTags": ["support", "product-a"],
  "accessVersion": 7
}
```

After verifying the assertion, the external service uses only its verified
`sub` as the exact document ID:

```http
GET /api/rest/{tenantId}/{appId}/projections/EffectiveKnowledgeAccess/{url-encoded-sub}
Authorization: Bearer <dedicated-runtime-api-key>
```

For signed `runtime_source = workspace:<workspaceId>`, call
`.../{url-encoded-sub}?workspaceId=<url-encoded-workspaceId>` (or send the
equivalent runtime-source header); omit the selector for `production`. Derive
it only from the verified claim. Keep the dedicated Runtime API key in the
external service, give it a role allowed only by this projection's `authRules`,
and never ship it in Page code. A missing document, `active !== true`, malformed
or empty `allowedTags`, an auth/read failure, or a runtime-source mismatch means
no access. Do not fall back to a list lookup, Production, another principal, or
browser-provided tags.

### Setup and tampering self-verification

1. Store the isolated signing secret under `credentialName` for every intended
   Production or Workspace source, and provision the same secret to the
   verifier through its secret manager.
2. Configure the endpoint, inspect its value-free assertion metadata with
   `list_adapter_endpoints`, validate the ChangeSet, and promote the artifacts
   needed by the test. Create the effective-access projection and dedicated
   Runtime API key when using the pull pattern.
3. Save the Page and call
   `preview_page(appId, slug, workspaceId?, docId?)` without inline source. Open
   the returned `agentViewUrl` or `browser.sessionUrl` to exercise the real
   authenticated Page adapter boundary; a data-URL preview cannot test these
   calls.
4. Give two test principals disjoint effective-access documents. In each
   authenticated session, deliberately submit the other principal's ID and tags
   and forge `X-CES-Principal-Assertion` with different capitalization. Results
   must follow only the session principal's verified `sub`, with no cross-user
   content.
5. Also test denied `authRules` and a missing/invalid signing credential. The
   downstream must receive no request, and the browser/capture/diagnostics must
   reveal no assertion, secret, or decoded claims.

See [Adapters](/extend-the-app/adapters) for the fixed JWT verifier contract
and [Permissions](/build-a-backend/permissions) for projection-key isolation.

## Revision-pinned session QA

Use `design__page__diagnose` with an exact `target` when validating Page
authorization and isolation. Do not combine exact mode with legacy
`workspaceId` or `deployment` selectors.

For Workspace QA, pass `kind: "workspace-revision"`, the Workspace ID, its exact
positive revision, and the 64-character Artifact-graph hash. For Production,
pass `kind: "production-release"`, the Release ID, and its 64-character
coherence token. Omit `sessionScenarios` to run the complete matrix: distinct
platform-generated principals A and B plus missing, expired, revoked,
wrong-app, and foreign-tenant sessions.

The target is rechecked before protected work and before URL delivery. Positive
scenarios receive distinct, short-lived, stateless, target-pinned Agent Page
Session URLs only after page authorization and module preflight. Negative
scenarios never receive a usable browser URL and never enter module preflight,
protected document resolution, or persistent session storage. The caller cannot
supply principal IDs, roles, attributes, tenant context, or environment selectors.
Positive sessions remain on the signed runtime source: a Workspace pin cannot
fall back to Production or another Workspace, and a Release pin cannot select a
Workspace or alternate deployment.

## Actions

Buttons and forms should map to command names users can understand. When a command needs confirmation or a reason, make that part of the command input instead of hiding it in component state.

## Playbook page dependencies

The Playbook presents Page usages as compact, bounded reference groups attached
to the Command or Data View they explain. At readable zoom a group shows a
representative Page and `+N` when more Pages are present. At low zoom the name
compacts to a Page glyph while the count and keyboard-sized controls remain
available. There is no global Page column or long dependency route across
unrelated scenario nodes.

Relationships are source-aware: Production reflects the effective live page,
while a Workspace reflects the effective Workspace page and its reachable
imported modules. If the Workspace has no page-source override, analysis follows
the same live fallback as the page runtime. Switching runtime source refreshes
the relationships rather than showing results from the previous source.

Playbook recognizes exact, verified `@ces/app-sdk` usage:

* `useCommand("PlaceOrder")` creates an `executes` relationship shown by a
  short Page → Command arrow.
* `useProjection("Orders")` creates a `reads` relationship shown by a short
  Data View → Page arrow.
* `binding: { projection: "Orders", ... }` on `PageConfig` is the authoritative
  `reads` relationship for `useDocument()`.

The arrows and endpoint types communicate the relationship without persistent
`executes` or `reads` edge chips. If the same canonical Page is used at several
targets, selecting any local occurrence highlights every occurrence and related
target. The Page drawer remains the complete source for Executes, Reads,
evidence, diagnostics, and navigation.

Calls in reachable shared Page modules count transitively. Dynamic hook targets,
missing commands or projections, an unbound `useDocument()`, and a page bundle
that cannot be built or inspected are reported as incomplete. The Playbook may
still show other verified relationships for that Page, but it never guesses an
edge from unresolved code. Keep command/projection names as string literals and
declare the Page binding before using `useDocument()` so the Slice remains
complete and navigable.

## Page preview

Use the Builder UI and Design MCP page-preview tools to verify pages before publish. Check:

* empty states
* loading states
* validation failures
* permission failures
* narrow mobile widths
* realistic projection data

The Builder preview is an authoring surface. After Cessy verifies Design
access, page-level `authRules` do not hide a page from its builder. Projection
and document authorization still use the current principal, and runtime
surfaces such as Operate, standalone pages, public shares, and Agent View keep
enforcing page authorization. Use an Operate or Agent View URL to test a
specific user's runtime permissions.

When an Operate URL selects a Workspace with `workspaceId`, its sidebar uses
the effective Workspace page configuration and keeps that selector on internal
Operate links. This lets a user who can access the Workspace navigate its
unpromoted pages without falling back to the published sidebar.

Run `design__page__validate` before previewing. It checks syntax and imports,
verifies that the default Page component can be mounted as `<PageComponent />`
without required props,
rejects named `@ces/app-sdk` imports or re-exports that the host module does not
provide, then type-checks JSX props on components imported through public Design
System aliases against the effective module source. TypeScript-detectable unknown
props, missing required props, and incompatible value shapes return structured
`ComponentContractError` diagnostics with a source location and suggested fix.
Do not declare Next.js-style `Page({ params })` or `Page({ searchParams })`
entrypoints. Use `useParams()` and `useDocument()` from `@ces/app-sdk` instead.
Unknown additional props are advisory everywhere. Missing required props and
incompatible value shapes are advisory while authoring a draft, then block
promotion only for changed or transitively affected pages. Successful authoring
and validation responses can therefore include a `warnings` array without an
MCP tool-execution error.
It also rejects unsupported object-literal `useProjection` options such as
`refreshKey`; use the result's no-argument `mutate()` method for explicit
revalidation.
When building a page without a comparable TSX example, call `get_frontend_sdk`
first. Its `authoringCatalog` lists the exact SDK runtime/type export names and
every default `@app/ui/*` component/export pair. Follow with
`design__page_design_system__inspect` when the app or Workspace may override or
extend the default UI catalog. Do not infer names such as `useNavigation`; use
the listed `useCesNavigation` hook.
Component catalog metadata is usage guidance; the module's exported TypeScript
types are the authoritative prop contract. Extra properties hidden inside
spread or broadly typed objects may not produce a TypeScript diagnostic and
still require review.

Workspace page-source edits, ChangeSet validation, and source-backed production
publication apply these focused gates. ChangeSet validation still raw-bundles
every page and globally enforces syntax, import/resource/security boundaries,
CSS parse/encoding integrity, and availability of component-contract validation.
Required Page entrypoint props, missing/incompatible Design System component
props, and App SDK export/option diagnostics block changed page sources and pages
affected through changed page resources; debt on an unchanged, unaffected source
is reported as a non-blocking warning. Hard-coded visual values,
undeclared/local CSS custom properties, inline styling, and similar Design System
conventions are review warnings rather than promotion blockers. Preview the real page
as well to catch runtime data, interaction, and visual issues outside these
focused checks.

The ChangeSet `page_quality_policy` also compares affected Page-quality findings
with the Workspace's frozen base. It reports each finding as `introduced`,
`changed`, or `baseline`; advisory findings remain warnings, while introduced
and changed technical integrity errors block promotion.
Inherited baseline errors stay visible in the validation details without
forcing unrelated Page rewrites. Changing the Design System `globalCss`
registration or a registered global CSS asset applies this comparison across
all affected Pages.

### Learn from an existing published page

Before building a page with no Workspace analog, call `read_page_source` with
the published page's `appId` and `slug` and omit `workspaceId`. The tool returns
the exact stored TSX, including its real `@ces/app-sdk` hooks and `@app/*`
component usage, plus live-source alignment evidence. Pass a line range when
only a focused excerpt is needed.

Published reads are read-only. Use `workspaceId` when reading source you intend
to patch so the response includes the current Workspace artifact hash. Pages
that exist only as legacy compiled bundles have no original TSX to return; for
those pages, `read_page_source` reports `page_source_missing` and
`read_deployment_files` remains the recovery path.

Published live-source alignment supports both legacy source-content hashes and
the dependency-sensitive Page Source Artifact hashes recorded by current
Release manifests. Cessy accepts the Release Artifact identity only when the
snapshot TSX exactly matches the published Page Source projection, avoiding a
false stale warning without hiding projection drift.

### Live source alignment before promotion

When a Workspace has a base page source, its diagnostics report two separate
facts. The top-level `liveSourceAlignment` result compares that base source with
the live manifest to detect a stale starting point. A proven base match is
reported as `workspace_base_source_matches_live_manifest`; a
`source_mismatch_with_live_deployment` warning means the base needs recovery or
review before promotion. The explicit
`isWorkspaceBaseSourceStaleWithLiveDeployment` field names the base-source
subject while preserving the legacy stale flag.

The nested `liveSourceAlignment.workspaceCurrentSourceAlignment` compares the
current Workspace artifact with the same live manifest. Its `status`,
`differsFromWorkspaceBase`, and `willReplaceLiveBundle` fields distinguish a
current/live match from an intentional pending replacement. In the latter case
the top-level `diagnostics` array contains
`workspace_source_pending_live_replacement`, making the bundle replacement
explicit without treating it as stale-source recovery. When no base page source
exists, top-level `liveSourceAlignment` instead retains the single
`current_source` recovery comparison and omits `workspaceCurrentSourceAlignment`
rather than claiming a proven base relationship. In that fallback path, a
`source_mismatch_with_live_deployment` warning refers to the current source.

Review this evidence with `read_page_source` or `inspect_page`. Before
promotion, also check `diff_workspace.liveSourceAlignments` and
`diff_workspace.diagnostics`; `validate_changeset` records the same
structured evidence in the `page_live_source_alignment` check details. A
warning is a stale-base safety signal when a base exists, or a current-source
recovery signal in the base-less fallback. The pending-replacement diagnostic is
normal when the reviewed Workspace edit is intended to replace Production.
The aggregate validation check is `passed` when at least one changed source has
conclusive live evidence, even if other entries are inconclusive. It is
`skipped` only when none of the changed sources are conclusive, and records each
alignment in `details`. A missing deployment, file set, or page module and an
unverified live module are inconclusive; `root_bundle_overrides_source` is a
conclusive warning and therefore keeps the check evaluated without manifest
proof.

## First release for a page-only app

When an app has no published config yet, create its Workspace with
`bootstrapEmpty: true`. A validated ChangeSet containing Custom Pages and/or
Design System resources can become the first production release while decision
models, events, and commands are all empty. Do not add placeholder domain
artifacts just to publish the page.

The runtime core must be either completely empty or complete. A partial domain
model is still rejected. Production promotion compiles every source-backed page
and verifies that every registered page has valid source or an eligible live
module; missing or invalid publication inputs block promotion before it creates
a Release or moves the environment pointer.

## Public sharing

Public custom page sharing is for no-login pages such as lead forms, customer intake forms, or campaign landing pages. Public shares render without the normal Cessy navigation and use a revocable URL under `/public/p/{appId}/{shareId}/...`.

Use public sharing only when the page is intentionally safe for unauthenticated visitors:

* keep `surfaces.standalone.enabled` set to `true` on the page
* use `surfaces.standalone.path` when the public URL should differ from the app sidebar slug
* test the Workspace `previewUrl` and `standaloneUrl` before enabling the public share
* expose only the projections and commands the visitor needs
* model validation and anti-abuse checks in the commands, not only in the page TSX

To create a public share from Design MCP:

1. Save the page with `upsert_page(workspaceId, page, source)` in a Workspace.
2. Inspect the page or use the latest artifact metadata to get the current `expectedHash`.
3. Call `enable_public_page_share(workspaceId, slug, expectedHash)`.
4. Validate and promote the ChangeSet to production before sending the returned `publicShareUrl` externally.

Enabling public sharing writes `surfaces.standalone.publicShare.enabled` and a URL-safe `shareId` into the Workspace page metadata. The public route reads the published production config, so a new public URL 404s until the ChangeSet is promoted.

To revoke a URL, call `disable_public_page_share(workspaceId, slug, expectedHash)`, validate, and promote the ChangeSet. The old URL stops rendering after the promoted production config disables the share.

## Public share permissions

Public shares do not authenticate a Cessy user. Page SDK reads and commands run as a constrained public principal with:

* role: `public`
* `$principal.attr.publicPage == true`
* `$principal.attr.publicShareId` set to the share id
* `$principal.attr.pageSlug`, `$principal.attr.appId`, and `$principal.attr.tenantId` set for the current page

Add explicit `authRules` to every projection and command the public page uses. Public projection and command endpoints reject resources without explicit authorization rules. Common rules include `$principal.attr.publicPage == true`, `"'public' in $principal.roles"`, or a stricter share-specific rule such as `$principal.attr.publicShareId == "share-id"`.

Page-level `authRules` are optional for backward compatibility, but add them when the page should render only for the public-share principal or a specific share id.

## Agent bubble and refresh

Runtime agents can use page refresh tools to keep a custom page aligned after agent actions. Use this for visible state changes, not as a substitute for reactive data loading in the page.

For in-page agent experiences, build tenant-owned UI with `useAgents` and
`useAgentChat` from `@ces/app-sdk`. `useAgents` loads the safe app agent catalog;
pass a selected published agent `id` to `useAgentChat`. A Workspace-only agent
appears in the source-aware catalog but becomes a chat target only after
promotion. When no conversation is selected, the first send creates a fresh
channel-backed page DM. The hook can also load a selected prior conversation,
returns AI SDK-compatible `messages`, streams
assistant text, reasoning, and tool-call state through the platform push
connection internally, and forwards the current page context privately with
each user message. The SDK does not provide a default chat bubble; each app owns
its operator extension UI.

Use `useChannelChat` when the page should chat inside an existing app channel instead of a separate page-chat/agent DM. It can bind directly by `channelId` or by an entity reference such as `{ kind: "DevTicket", id: ticketId }`, exposes a clear `missing` state unless `createIfMissing` is enabled, sends messages to that same channel with private page context, and streams reasoning, tool, text, finish, and error parts in the same `messages` shape as `useAgentChat`. Pass the existing participant's `agentId` to target that agent without changing the user-visible message; `mentionAgent: true` additionally renders the technical mention in the channel.

Custom Pages should not call Runtime REST channel endpoints or internal
`/api/apps/...` page-chat routes directly. The host-provided SDK owns page auth,
each hook's declared runtime-source behavior, streaming, and private page context. External
frontends and BFFs use Runtime REST instead. See the
[Frontend SDK Reference](/build-a-frontend/frontend-sdk-reference) for the complete export contract
and boundary guidance.

## Handoff

A page handoff should name the projections and commands the page uses, the target environment, the preview URL or page slug, and any BFF endpoints involved. For public shares, also include the `publicShareUrl`, the share revocation path, and the projection, command, and page-level auth rules that make the no-login flow safe.