Skip to navigation

Build Custom Pages

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:

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

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

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 for the fixed JWT verifier contract and 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 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.