> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.cessy.ai/build-a-frontend/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": "", "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 ``` For signed `runtime_source = workspace:`, call `.../{url-encoded-sub}?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 `` 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. > Build app-specific Cessy pages that use generated runtime contracts instead of duplicating business rules. ## Docs - [Frontend SDK Reference](https://docs.cessy.ai/build-a-frontend/frontend-sdk-reference.md): Use the host-provided @ces/app-sdk contract for authenticated, runtime-source-aware Cessy Custom Pages. - [Frontend Prompting](https://docs.cessy.ai/build-a-frontend/frontend-prompting.md): Prompt AI builders to create Cessy custom pages that respect runtime contracts, permissions, data states, and page preview feedback. - [BFF Pattern](https://docs.cessy.ai/build-a-frontend/bff-pattern.md): Use a Backend-for-Frontend with Cessy custom pages when UI orchestration belongs outside the event-sourced model.