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:
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:
After verifying the assertion, the external service uses only its verified
sub as the exact document ID:
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
- Store the isolated signing secret under
credentialNamefor every intended Production or Workspace source, and provision the same secret to the verifier through its secret manager. - 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. - Save the Page and call
preview_page(appId, slug, workspaceId?, docId?)without inline source. Open the returnedagentViewUrlorbrowser.sessionUrlto exercise the real authenticated Page adapter boundary; a data-URL preview cannot test these calls. - 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-Assertionwith different capitalization. Results must follow only the session principal’s verifiedsub, with no cross-user content. - Also test denied
authRulesand 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 anexecutesrelationship shown by a short Page → Command arrow.useProjection("Orders")creates areadsrelationship shown by a short Data View → Page arrow.binding: { projection: "Orders", ... }onPageConfigis the authoritativereadsrelationship foruseDocument().
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.enabledset totrueon the page - use
surfaces.standalone.pathwhen the public URL should differ from the app sidebar slug - test the Workspace
previewUrlandstandaloneUrlbefore 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:
- Save the page with
upsert_page(workspaceId, page, source)in a Workspace. - Inspect the page or use the latest artifact metadata to get the current
expectedHash. - Call
enable_public_page_share(workspaceId, slug, expectedHash). - Validate and promote the ChangeSet to production before sending the returned
publicShareUrlexternally.
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.publicShareIdset to the share id$principal.attr.pageSlug,$principal.attr.appId, and$principal.attr.tenantIdset 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.