YYayaw
Docs

Control Plane

Production MCP and local stdio control-plane access for MCP clients and automation.

Overview

Yayaw exposes a production control plane through MCP. Codex is one supported client, but the endpoint is designed for any trusted MCP client or automation runner.

  • Production and staging use Streamable HTTP at /api/mcp.
  • Local development can use stdio with bun --conditions react-server --env-file=.env src/lib/scripts/mcp/yayaw-mcp-server.ts.
  • Production MCP requests can authenticate with either a Better Auth API key or a Better Auth OAuth access token. Both paths use explicit control-plane permissions.
  • Runtime data-plane requests use the same bearer actor format but can be scoped to the narrower dynamic-data-runtime API key resource.
  • The control-plane-mcp-enabled site setting is the server-side kill switch.
  • Underlying Yayaw authorization still runs through can(...) or equivalent scoped membership checks.

API Key Permissions

API keys use Better Auth API key permissions under the control-plane resource:

  • read: status, discovery, resources, list/get tools.
  • write: draft/data/media/design-token mutations.
  • publish: publish/archive lifecycle tools.
  • admin: audit and feature-flag administration.

Admin does not bypass Yayaw resource authorization. It only satisfies the control-plane permission gate.

Hot runtime callers should prefer the narrower dynamic-data-runtime resource:

  • read: execute read-only deployed dynamic runtime routes.
  • write: execute write deployed dynamic runtime routes such as heartbeat, status updates, pairing claims, or metrics ingestion.

dynamic-data-runtime:write implies runtime read access when the Developer UI normalizes permissions. Existing full MCP keys remain compatible with the data plane: control-plane:read can execute read runtime routes, while control-plane:write or control-plane:admin can execute write runtime routes. All paths still run Yayaw dynamic-data authorization after API-key permission checks, so bearer scopes never replace group-based resource authorization.

Production MCP Connection

Create a production MCP key, store it in your client environment as YAYAW_MCP_API_KEY, then configure the MCP client. Codex can use:

[mcp_servers.yayaw-prod]
url = "https://yayaw.app/api/mcp"
bearer_token_env_var = "YAYAW_MCP_API_KEY"

Before connecting, enable the site setting:

bun run seed

Then set control-plane-mcp-enabled to true from Admin settings or with the control-plane flag tool using an existing admin key.

OAuth MCP Connection

Yayaw also exposes MCP-compatible OAuth 2.1 discovery for ChatGPT Apps and other clients that follow the MCP authorization flow. OAuth supports authorization code with PKCE and refresh tokens; machine-to-machine access should continue to use MCP API keys.

  • Protected resource metadata: /.well-known/oauth-protected-resource/api/mcp
  • Authorization server metadata: /.well-known/oauth-authorization-server/api/auth
  • OpenID metadata: /.well-known/openid-configuration/api/auth
  • Authorization server base: /api/auth
  • MCP resource/audience: /api/mcp

The MCP endpoint returns a WWW-Authenticate challenge on unauthenticated or invalid OAuth requests so clients can discover the protected resource metadata. The remote MCP CORS policy allows the exact https://chatgpt.com origin and exposes the challenge header to browser clients while rejecting unrelated origins. OAuth tokens are verified against the Better Auth JWKS, issuer, audience, expiry, and control-plane scopes before a tool runs.

The OAuth provider seeds the canonical /api/mcp RFC 8707 resource at startup. Dynamic clients may request that enabled resource without a pre-provisioned client-resource link, but they must still complete user authentication and consent before Better Auth issues a scoped token.

The OAuth provider intentionally advertises the full MCP permission set:

control-plane:read control-plane:write control-plane:publish control-plane:admin

This keeps the MCP tool surface unchanged for trusted linked clients. Tool handlers still enforce the required control-plane permission and the underlying Yayaw resource authorization for every operation, so OAuth scopes never replace application authorization.

For published ChatGPT Apps, ensure NEXT_PUBLIC_BASE_URL is the public HTTPS origin before generating metadata. ChatGPT redirects back to https://chatgpt.com/connector/oauth/{callback_id}; the registered OAuth client must allow that redirect URI.

Local MCP Connection

Local stdio is useful for development and does not require opening a public endpoint. Codex can register it with:

codex mcp add yayaw-local -- bun --conditions react-server --env-file=.env src/lib/scripts/mcp/yayaw-mcp-server.ts

For local stdio, set YAYAW_MCP_API_KEY to test production-like API key verification. Without a key, the launcher creates a local development actor using YAYAW_MCP_LOCAL_USER_ID or local-codex. Local development audit events store that synthetic user id in metadata and leave actor_user_id empty, so MCP dry-runs do not require a matching Better Auth user row. For dynamic-data smoke runs, that trusted local stdio actor can use its local control-plane:admin permissions to pass dynamic-data control-plane checks without a database-backed user. HTTP, OAuth, API-key, preview, and production actors still require the normal Yayaw authorization decision for every dynamic-data operation.

Run the generic dynamic-data MCP smoke before or after bundle work:

bun run mcp:dynamic-data-smoke

The command starts the local stdio MCP server, lists tools, calls yayaw_dynamic_data_ui_capabilities, and verifies the bundle.postInstall.verification checklist contract plus representative Yayaw Table, generated form, detail panel, dynamic navigation, and dashboard.operatorReadiness capabilities. The smoke output includes the operator readiness admin control-plane root, dynamic section route pattern, attention states, summary fields, and MCP/runtime handoff endpoints so deployed runs can show where dashboard surfaces are visible. The dashboard loader also reads the same target schema readiness before listing models, renders missing required control-plane tables in the operator panel, and skips registry queries while the schema is blocked. Set YAYAW_MCP_SMOKE_TRANSPORT=http, YAYAW_MCP_URL=https://yayaw.app/api/mcp, and YAYAW_MCP_API_KEY to run the same smoke against a deployed endpoint. Set YAYAW_MCP_SMOKE_VALIDATE_BUNDLE=1 to locally parse, normalize, and validate the configured bundle fixture without requiring DB connectivity or dynamic-data authz on the target endpoint. Set YAYAW_MCP_SMOKE_BUNDLE_DRY_RUN=1 only when the target environment has the DB and authz state needed to dry-run the bundled product-neutral resource models. By default that optional dry-run loads src/lib/scripts/mcp/fixtures/dynamic-data-resource-bundle.json; set YAYAW_MCP_SMOKE_BUNDLE_FILE to point at another full yayaw_dynamic_data_bundle_apply JSON input. For external product bundles kept outside the repository, prefer the generic mcp:dynamic-data-bundle-* scripts: mcp:dynamic-data-bundle-smoke, mcp:dynamic-data-bundle-target-readiness, mcp:dynamic-data-bundle-dry-run, mcp:dynamic-data-bundle-apply, mcp:dynamic-data-bundle-installed-smoke, and mcp:dynamic-data-bundle-seeded-smoke. Those scripts set YAYAW_MCP_SMOKE_REQUIRE_BUNDLE_FILE=1, so the run fails if YAYAW_MCP_SMOKE_BUNDLE_FILE is missing instead of accidentally falling back to the built-in fixture. Run bun run mcp:dynamic-data-foundation-smoke to validate the generic foundation fixture at src/lib/scripts/mcp/fixtures/dynamic-data-foundation-bundle.json. That script sets YAYAW_MCP_SMOKE_VALIDATE_FOUNDATION=1, which requires a bundle to cover native tables, dashboard UI and navigation, a dynamic principal registry, principal references and grants, active-grant constraints, runtime routes, resource endpoint child models, resource trust bindings, heartbeat with composite idempotency keys, workflow lifecycle status routes with terminal side effects, pairing claims with hashed secrets and attempt counters, metrics ingest with rollups, principal rate limits, principal-scoped guards, cross-model effects, realtime topics, projections, and short-retention raw samples. The foundation fixture is test data, not a built-in Yayaw application model. Do not apply it to production unless an external product intentionally adopts that exact contract. Product integrations should provide their own bundle file through the generic mcp:dynamic-data-bundle-* commands. Run bun run mcp:dynamic-data-foundation-dry-run when the target endpoint has dynamic-data DB tables and dynamic-data:manage authz for the MCP actor. It uses the same foundation fixture and validation checks, then calls yayaw_dynamic_data_bundle_apply with dryRun=true so operators can confirm target readiness before a real publish/deploy apply. Run bun run mcp:dynamic-data-target-readiness before the first real apply on a target endpoint. It loads the generic foundation fixture, validates the local contract, discovers MCP tools, reads dynamic-data capabilities, calls yayaw_dynamic_data_target_readiness for the target DB schema, lists models in the bundle scope only when that schema preflight is ready, and dry-runs yayaw_dynamic_data_bundle_apply. The command prints a targetReadiness report with ready or blocked status and exits non-zero when a required step fails, so missing authz, migrations, DB tables, or bundle compatibility are visible before any mutation. Run bun run mcp:dynamic-data-foundation-apply only after the target readiness report is ready. The command reuses the same foundation fixture, keeps readiness enabled, requires the real-apply confirmation YAYAW_MCP_SMOKE_BUNDLE_APPLY_CONFIRM=publish-deploy, sends an audit reason, and then calls yayaw_dynamic_data_bundle_apply with dryRun=false, publish=true, and deploy=true. If readiness is blocked, it reports bundleApply.status: skipped and exits before mutation. The apply script also sets YAYAW_MCP_SMOKE_POST_INSTALL_VERIFY=1, so after a successful apply it executes the safe MCP tool calls from postInstall.verification.checklist and classifies manual or product-data-dependent steps as skipped with reasons such as manual_step, requires_input, or requires_values. Run bun run mcp:dynamic-data-foundation-installed-smoke after a successful apply to verify the deployed foundation as a whole. The command derives the expected model list from the same generic fixture, verifies yayaw_dynamic_data_models_list reports each model as published, deployed, and valid, reads the runtime manifest, checks generated dashboard routes, navigation, normalized Yayaw Table table, form, detail settings, and extension entity bindings, compares declared UI control ids against the installed runtime manifest, compares declared runtime route contracts against yayaw_dynamic_data_runtime_routes including metrics-ingest rollups, compares declared realtime event-feed contracts against runtimeEventFeeds from the installed manifest, queries each model, reads runtime events, resolves every declared runtime route, and calls yayaw_dynamic_data_entity_extensions for each declared extension target to prove that extension discovery matches the deployed manifest. Set YAYAW_MCP_SMOKE_MODEL_SLUG, YAYAW_MCP_SMOKE_MODEL_SCOPE, and optional YAYAW_MCP_SMOKE_ORG_ID to verify an already deployed dynamic model through the manifest, generated dashboard route, normalized manifest fields, system columns, ui.table, ui.form, ui.detail, ui.nav, bounded record query, runtime event-feed contract, runtime event read, and runtime route resolution. Add YAYAW_MCP_SMOKE_RUNTIME_ROUTE_EXAMPLES_JSON or YAYAW_MCP_SMOKE_RUNTIME_ROUTE_EXAMPLES_FILE to dry-run execute selected runtime routes during the installed-model smoke. Each example is a JSON object with modelSlug plus routeKey, routeId, templatePath, or method, and a concrete path, optional recordId, values, filters, sort, search, recordLimit, recordOffset, expectedDataVersion, or reason. This keeps product-specific route payloads outside the repository while letting MCP prove that generic deployed routes are resolvable and executable. External bundle fixtures can also include top-level smoke.runtimeRouteExamples with the same example shape. The smoke runner reads those examples for installed-model and installed-bundle verification, while stripping smoke before calling yayaw_dynamic_data_bundle_apply; examples provided through YAYAW_MCP_SMOKE_RUNTIME_ROUTE_EXAMPLES_JSON or YAYAW_MCP_SMOKE_RUNTIME_ROUTE_EXAMPLES_FILE take priority over embedded bundle examples. External fixtures can also include smoke.recordSeeds, an ordered array of objects with modelSlug, optional recordId, optional scope/orgId, an optional label, and values. Set YAYAW_MCP_SMOKE_RECORD_SEEDS=1 to preflight those seeds through yayaw_dynamic_data_record_create with dryRun=true. To create records for an end-to-end target smoke, also set YAYAW_MCP_SMOKE_RECORD_SEEDS_APPLY=1, YAYAW_MCP_SMOKE_RECORD_SEEDS_CONFIRM=create-records, and YAYAW_MCP_SMOKE_RECORD_SEEDS_REASON for the audit log. This lets product bundles carry their own test data while keeping Yayaw's repository product neutral. Set YAYAW_MCP_SMOKE_RECORD_SEEDS_RESET=1 to delete existing seed records before creation by using stable recordId values from the bundle in reverse seed order. Reset defaults to dryRun=true, ignores records that are already missing, and requires YAYAW_MCP_SMOKE_RECORD_SEEDS_RESET_APPLY=1, YAYAW_MCP_SMOKE_RECORD_SEEDS_RESET_CONFIRM=delete-records, and YAYAW_MCP_SMOKE_RECORD_SEEDS_RESET_REASON for real deletions. Set YAYAW_MCP_SMOKE_RECORD_SEEDS_CLEANUP=1 to run yayaw_dynamic_data_record_delete for records created by the same smoke run. Cleanup defaults to dryRun=true; real cleanup requires YAYAW_MCP_SMOKE_RECORD_SEEDS_CLEANUP_APPLY=1, YAYAW_MCP_SMOKE_RECORD_SEEDS_CLEANUP_CONFIRM=delete-records, and YAYAW_MCP_SMOKE_RECORD_SEEDS_CLEANUP_REASON so product smoke bundles can verify write paths without leaving validation records behind. The bundled dynamic-data-foundation-bundle.json fixture includes product-neutral smoke.recordSeeds for its resource, endpoint, trust, session, and metric models so the generic foundation contract has a reusable write-path example. It also includes product-neutral smoke.runtimeRouteExamples for the deterministic heartbeat, trust, session-status, and metrics-ingest routes. The smoke runner executes those bundled route examples only when real record seed creation is enabled with YAYAW_MCP_SMOKE_RECORD_SEEDS_APPLY=1, or when an operator explicitly sets YAYAW_MCP_SMOKE_RUNTIME_EXAMPLES_FROM_BUNDLE=1, so plain installed-bundle checks do not depend on pre-existing seed records. The same foundation fixture wires product-neutral ui.table.rowActions and ui.detail.actions to heartbeat, endpoint-check, trust-seen, and session-end runtime routes. The foundation validator now checks runtimeUiActions so the generic bundle keeps proving that MCP can describe native tables, runtime routes, and actionable backend UI without repository code. After the foundation bundle is deployed on a target with dynamic-data tables, run bun run mcp:dynamic-data-foundation-seeded-smoke to combine installed bundle verification, seed reset, seed creation preflight, and cleanup preflight in one product-neutral command. Add the documented *_APPLY, *_CONFIRM, and *_REASON environment variables only when intentionally writing and deleting seed records on that target. Set YAYAW_MCP_SMOKE_ENTITY_TARGET with optional YAYAW_MCP_SMOKE_ENTITY_SCOPE, YAYAW_MCP_SMOKE_ENTITY_ORG_ID, YAYAW_MCP_SMOKE_ENTITY_TARGET_ID, YAYAW_MCP_SMOKE_ENTITY_BINDING_KINDS, YAYAW_MCP_SMOKE_ENTITY_INCLUDE_COUNTS=1, and YAYAW_MCP_SMOKE_ENTITY_MIN_EXTENSIONS to verify yayaw_dynamic_data_entity_extensions for deployed dynamic models that extends or references native or product entities. This is the generic path for surfacing extensions on native dashboard pages without product React code.

Key Management

For a fully trusted MCP operator key, use the full control-plane permission set:

control-plane:read,write,publish,admin

The dashboard Developer settings page uses Yayaw's custom API key block rather than the generic Better Auth UI, because Better Auth treats API key permissions as server-only fields. Use the Yayaw block to create new MCP or runtime keys with explicit scopes, or to repair an existing key by setting the Full API access preset. The preset writes:

control-plane:read,write,publish,admin
dynamic-data-runtime:read,write

For a runtime-only data-plane key, leave control-plane permissions empty and grant:

dynamic-data-runtime:read,write

For a key that can both configure models through MCP and call hot runtime routes, combine both resources:

control-plane:read,write,publish,admin
dynamic-data-runtime:read,write

The UI can update permissions on an existing key without revealing or rotating the secret. Use that only when the current secret is already stored safely, because Better Auth still shows the secret only once.

Organization-owned keys are the default choice for MCP clients operating a customer/workspace scope. They are issued for the active organization and can be managed by Better Auth organization owners, admins, and managers. They carry issuer metadata so MCP can keep running Yayaw authorization as the issuing user, and they are constrained to that organization for scoped page, CMS data, and media operations.

Personal/user-owned keys are superadmin-only and are intended for global Yayaw administration. Use them for global content, feature flags, or cross-organization operations only when that is intentional.

Issue a user-owned MCP API key:

bun run mcp:key -- issue --email admin@example.com --permissions read,write,publish,admin

Issue an organization-owned MCP API key:

bun run mcp:key -- issue --email owner@example.com --organization-slug acme --permissions read,write,publish,admin

List a user's keys and their stored permissions:

bun run mcp:key -- list --email admin@example.com

List an organization's keys and their stored permissions:

bun run mcp:key -- list --organization-slug acme

Inspect one key by id:

bun run mcp:key -- inspect --key-id <api-key-id>

Inspect the secret currently configured for a local MCP client:

YAYAW_MCP_API_KEY=<secret> bun run mcp:key -- inspect --token-env YAYAW_MCP_API_KEY

Revoke a key:

bun run mcp:key -- revoke --key-id <api-key-id>

The issue command prints the secret once. Store it in a secret manager or the local MCP client environment; do not commit it.

Tools

Core:

  • yayaw_capabilities
  • yayaw_status
  • yayaw_audit_list
  • yayaw_deploy_status

CMS and pages:

  • yayaw_pages_list, yayaw_pages_get, yayaw_pages_validate, yayaw_pages_diff
  • yayaw_pages_publish, yayaw_pages_archive, yayaw_pages_delete
  • yayaw_pages_create_draft, yayaw_pages_save_draft (disabled maintenance bypasses)
  • yayaw_components_list, yayaw_component_get, yayaw_component_import, yayaw_component_recipe_import
  • yayaw_components_sync_registry, yayaw_component_recompile
  • yayaw_component_publish, yayaw_component_delete
  • yayaw_sections_list, yayaw_sections_get, yayaw_sections_create_draft
  • yayaw_sections_validate, yayaw_sections_save_draft
  • yayaw_sections_publish, yayaw_sections_archive
  • yayaw_kibo_sections_generate
  • yayaw_cms_design_prepare, yayaw_cms_design_resume, yayaw_cms_design_guidance_get, yayaw_cms_design_context_renew, yayaw_cms_design_inventory_refresh
  • yayaw_cms_prototypes_register, yayaw_cms_prototype_runtime_previews_get, yayaw_cms_prototype_runtime_review_complete, yayaw_cms_prototype_select
  • yayaw_cms_page_design, yayaw_cms_page_rework, yayaw_cms_page_review, yayaw_cms_page_review_complete
  • yayaw_cms_page_design_legacy_admin (disabled admin compatibility bypass)
  • yayaw_cms_personalization_requests_list, yayaw_cms_personalization_request_get, yayaw_cms_personalization_request_status_update, yayaw_cms_personalization_request_delete, yayaw_cms_personalization_requests_prune (admin only)
  • yayaw_data_models_list, yayaw_data_schema_get, yayaw_data_entry_get
  • yayaw_data_entry_upsert, yayaw_data_entry_publish
  • yayaw_dynamic_data_ui_capabilities
  • yayaw_dynamic_data_target_readiness
  • yayaw_dynamic_data_reference_options
  • yayaw_dynamic_data_models_list, yayaw_dynamic_data_model_get
  • yayaw_dynamic_data_model_revisions, yayaw_dynamic_data_model_revision_diff, yayaw_dynamic_data_model_revision_restore
  • yayaw_dynamic_data_entity_extensions
  • yayaw_dynamic_data_runtime_manifest
  • yayaw_dynamic_data_runtime_routes
  • yayaw_dynamic_data_runtime_events
  • yayaw_dynamic_data_runtime_route_resolve
  • yayaw_dynamic_data_runtime_execute
  • yayaw_dynamic_data_model_save, yayaw_dynamic_data_model_publish
  • yayaw_dynamic_data_model_deployment_plan, yayaw_dynamic_data_model_deploy
  • yayaw_dynamic_data_model_archive, yayaw_dynamic_data_model_delete
  • yayaw_dynamic_data_bundle_apply
  • yayaw_dynamic_data_records_query, yayaw_dynamic_data_record_get
  • yayaw_dynamic_data_record_create, yayaw_dynamic_data_record_update, yayaw_dynamic_data_record_delete
  • yayaw_email_templates_list, yayaw_email_template_get
  • yayaw_email_template_save, yayaw_email_templates_sync_system
  • yayaw_org_domains_list, yayaw_org_domain_add, yayaw_org_domain_check
  • yayaw_org_domain_set_primary, yayaw_org_domain_archive

Site operations:

  • yayaw_billing_products_list, yayaw_billing_product_update
  • yayaw_stripe_discounts_list, yayaw_stripe_discounts_sync
  • yayaw_media_list, yayaw_media_search, yayaw_media_generate, yayaw_media_upload, yayaw_media_import_url, yayaw_media_upload_prepare, yayaw_media_upload_complete
  • yayaw_design_tokens_get, yayaw_design_tokens_save
  • yayaw_flags_list, yayaw_flags_update

When adding a tool, prefer a service-layer implementation that can also be used by UI server actions or scripts. Tool handlers should stay thin: validate input, check control-plane permissions, call the shared service, and write audit data.

Resources

  • yayaw://status
  • yayaw://schemas
  • yayaw://pages/{scope}/{slug}
  • yayaw://data/{scope}/{modelSlug}/{entrySlug}
  • yayaw://email-templates/{slug}
  • yayaw://audit/recent

Billing, discount, media, design-token, feature-flag, and organization-domain workflows are exposed as tools rather than MCP resources. Organization-scoped page and data reads should use the tools when an explicit orgId is required.

Safety Rules

Production writes require all of the following:

  • control-plane-mcp-enabled is enabled.
  • A valid Better Auth API key or OAuth access token is supplied as Authorization: Bearer <token>.
  • The key or OAuth token has the required control-plane permission.
  • The actor passes the underlying Yayaw resource authorization checks.
  • The tool input includes reason.

Organization-owned keys are additionally limited to their owning organization for scoped content/media operations. Personal keys still require the seeded Yayaw apikey:manage permission to be issued from the dashboard.

Organization public-domain tools follow the same organization boundary. Organization-owned keys operate only on their owning organization; personal keys must pass an explicit orgId. Domain mutations require organization:update or organization-settings:manage, a reason, and use dryRun for preflight checks. PUBLIC_DOMAIN_PROVIDER=vercel attaches the hostname to the shared Vercel project, reads recommended DNS records, and verifies Vercel ownership challenges. PUBLIC_DOMAIN_PROVIDER=manual-dns generates a TXT ownership token and checks public DNS for the expected TXT plus configured CNAME/A hints.

yayaw_deploy_status returns provider-neutral deployment metadata. On Vercel it reports Vercel runtime fields; on self-hosted deployments it reports static DEPLOYMENT_URL, DEPLOYMENT_ENV, DEPLOYMENT_GIT_COMMIT_SHA, and DEPLOYMENT_GIT_COMMIT_REF values when configured.

Component writes are DB-backed catalog imports or recompiles from stored source snapshots. yayaw_component_recipe_import accepts only bounded declarative recipes backed by checked-in safe runtime kinds, creates a draft_ready revision, and leaves publication explicit. MCP component tools never mutate repository files in production and never execute DB-stored source code at runtime. Use yayaw_components_list(includeSchemas=true) to inventory exact prop schemas, component references, and the allowlisted creative-composition node/property/token/icon contract before composing sections. That advertised contract also lists its server-inferred creative capability names, per-node prop value types and required fields, explicit typography family roles, stable semantic surface roles, and the compatibility aliases that cannot attest semantic intent. It also exposes the exact registered CMS form handlers. The sole registered handler is POST /api/cms/forms/personalization-request with multipart encoding. It requires email (email), firstName (text), consent (checkbox), and photo (native images upload), while birthDate (date) and message (textarea) are optional. Names, control types, and required flags must match exactly. The photo must be JPEG, PNG, or WebP and at most 8 MB; it is stored privately. The submission creates a personalization request for operator follow-up, not a checkout, order confirmation, or payment. Every other handler or action must use email-fallback instead of inventing same-origin POST or multipart behavior.

Admin-only request operations are yayaw_cms_personalization_requests_list, yayaw_cms_personalization_request_get, yayaw_cms_personalization_request_status_update, yayaw_cms_personalization_request_delete, and yayaw_cms_personalization_requests_prune. Get issues a short-lived encrypted same-origin photo URL that exposes no storage coordinates, is served as a no-store attachment, and expires with a safety margin before retention. A terminal retention window returns no URL. Delete permanently removes one request and its private photo, and prune supports dry-run before removing requests whose 180-day retention deadline has expired. The Page AI worker runs bounded retention and retryable object cleanup independently from AI jobs. These five PII operations require control-plane:admin, a platform actor with no organization scope, and the global Yayaw user:manage authorization. An organization administrator or an admin scope claim without that RBAC grant cannot access them. For creative-composition, runtime inputs are limited to propSchema: apiSchema and harnessSchema must be omitted or empty, fixtures must be empty, and their related API, harness, and fixture settings cannot carry hidden runtime content. Every registered prototype candidate declares a non-empty requiredCapabilities manifest. Dry-run blocks persistence until exact published recipe revisions at the roots of mapped composition sections reproduce the exact registered regional structural, implementation, and render manifests. The candidate's computed manifest must contain both display_typography and responsive_recomposition, one or more structural signatures from layered_composition, sticky_narrative, custom_grid_ratios, continuous_canvas, or compact_band, and one or more art-direction treatments from image_art_direction, paper_collage, watercolor_wash, media_texture, or decorative_shapes. text.style: "note" alone does not infer paper_collage. At least one signature region must infer display typography, responsive recomposition, one structural signature, and one art-direction treatment together from its exact recipe. A new_recipe region requires a same-context created revision whose compositionHash topology is not already present on a published creative-composition in the prepared inventory, while reuse_published requires its exact declared published component. A nested component, different tree with the same coarse capability labels, or changed instance bindings fails; self-attestation does not count. The advertised contract includes canvas.height values content and band plus bounded mobileGap; responsive image.mobileAspect, image.desktopAspect, and image.height; decoration.treatment: "wash"; paper-like frame.material, bounded frame.rotation and placement.rotation, text.style: "note"; text.weight; visibility.show; the semantic disclosure node; explicit section.overflow and frame.overflow; bounded image.blend and image.opacity; and bounded control appearance values. Visible section/frame overflow infers continuous_canvas only when a descendant placement, rotated or taped frame, or large/washed decoration can actually cross the boundary. A non-default image blend (multiply, screen, or soft-light) or non-solid opacity (soft or faint) infers media_texture. Actions and submits accept bare, outline, paper, solid, or underline; fields accept bare, line, outline, or paper; uploads accept bare, dashed, or paper. These infer compact_band, responsive_media, watercolor_wash, paper_collage, typographic_weight, responsive_recomposition, and responsive_navigation as applicable. A client must create a feasibility ledger for every prototype region against those exact schemas. Each region declares exact allowlisted nodes with canonical paths (tree, then tree.children.N recursively), bounded props, an exact declarative recipePropSchema, an equal-key recipePropNames list, and exact recipePropBindings. Required props without defaults need bindings, binding keys must belong to the schema, and every image.src must be exactly $props.<identifier> whose prop is bound as media_asset or localized_media_asset to durable prepared-inventory media. mediaAssetIds must be the exact deduplicated set of IDs used by all media bindings. Yayaw rebuilds the complete recipe and validates it through the canonical runtime contract. It computes feasibility.compositionHash from node names, canonical paths, and structural props only, excluding content, media, actions, surface/tone palette choices, and other non-structural props while normalizing runtime references. It separately computes feasibility.implementationHash from the exact prop names/schema/tree and feasibility.renderManifestHash from that implementation plus its exact instance bindings; clients submit none of these hashes. compact_band requires height: "band"; responsive_media requires distinct non-auto mobile and desktop aspects without a fixed height; and responsive_navigation requires a mobile disclosure plus a non-empty desktop branch. Responsive order props (desktopOrder or mobileOrder) require a direct grid or stack parent; responsive span props (desktopSpan or mobileSpan) require a direct grid parent. A qualifying ancestor is not enough. Grid, directional stack, placement, and sticky recomposition share the md breakpoint. mobileSpan: "2" is rejected because the mobile grid exposes one explicit base column. Aggregate capability coverage cannot make a blocked region feasible. The canonical creative renderer passes exact bound props through unchanged, including names used by legacy catalog fixtures such as value, page, checked, disabled, and pressed. When a prototype or draft preview lacks live form context, it renders the registered form's exact layout and controls inside an inert, non-submittable wrapper instead of changing their geometry. Every prototype-visible text, link, label, placeholder, image alt, or upload message must be a direct $props.<identifier> sink. Text-like bindings are exact localized literals for all prepared locales; links use bounded safe literal or localized targets. Prototype-critical global-data bindings and repeat/$item/$index are rejected, so every visible repeated item is expanded into explicit planned nodes.

CMS page design is a contextual client-orchestrated workflow. Read and approve any broad design-token change first, then start with yayaw_cms_design_prepare; answer its material questions through MCP elicitation or yayaw_cms_design_resume. Its durable visualIntent owns the ambition gate: a new or evolved direction derives a creative strategy and cannot be downgraded to avoid the three prototypes. The client loads each required actor-bound guidance stage through yayaw_cms_design_guidance_get: brief, composition, and critique, plus rework for rework sessions. If a required broad token change is discovered after preparation, discard the context, save the layer, and prepare again; inventory refresh cannot move a context onto another token hash.

Before prototype registration, finalize every story or recipe image through yayaw_media_upload_prepare and yayaw_media_upload_complete, or import it through yayaw_media_import_url, with the active designContextId. Perform one pre-selection yayaw_cms_design_inventory_refresh after that media batch. The refresh may append only finalized same-context media while every catalog and design-token baseline remains unchanged. Candidate-board uploads and later runtime-review screenshots are queried directly by asset ID and must not trigger inventory refresh.

For creative work, upload three inspected complete prototype boards and call yayaw_cms_prototypes_register with kind: "generated_set_v2". Registration requires exactly three distinct durable images with server-verified file hashes and dimensions of at least 1200 × 900. A user-supplied prototype can anchor one candidate but never reduces the set below three. Each board includes every intended section at sufficient fidelity. Yayaw also computes a normalized 16 × 16 grayscale edge-matrix fingerprint from every stored full board and rejects clones or boards below the minimum structural distance. This coarse normalized-structure guard is never sufficient proof of a distinct direction or fidelity; it supplements the three server-owned signatures, pairwise critique, visual inspection, and substantive Visual QA. All candidates declare the same ordered stable region IDs and labels for later prototypeRegionRef evidence, distinct originality evidence for silhouette, typography, narrative rhythm, metaphor, and signature device, plus the structured applied decisions from every required guidance stage. Each unique directive is classified as prototype_applicable, implementation_only, or not_applicable; prototype-applicable directives include one observed passing artifact location for every candidate. Submitted classifications and implementation gates must match the server-owned directive requirements. Version-2 traces expose an optional deterministic verificationCheckId owned by the server. Later page plans submit only the exact implementation directive IDs through guidanceVerification; Yayaw runs the checker at its fixed gate and persists the hashed proof. Clients cannot invent a checker, evidence path, or review section. Not-applicable decisions are accepted only for server-allowed rules, require a specific reason, and cannot exceed the bounded per-stage ratio. Submitted decision IDs must cover every directive returned by each required loaded stage exactly once; omitted, duplicated, or invented IDs fail registration. Yayaw stores those decisions with server-owned hashes of the exact selected guidance content. Rejection or a requested combination requires a replacement set of exactly three registered and runtime-reviewed boards. A candidate with an unavailable region primitive, asset, font, interaction, handler, responsive behavior, or fallback is blocked before presentation and registration; the client replaces the complete set of three instead of promising a later approximation. The same registration call includes exactly three structured pairwise comparisons: A/B, A/C, and B/C. Each pair must be materially distinct across silhouette, typography, narrative rhythm, visual metaphor, signature device, and mobile/theme resolution, and must cite both candidates with exact <artifactId>#<regionId> evidence for every ordered region. The server also requires three distinct A/B/C structural, implementation, and render signatures. Ordered regional compositionHash values form the structural signature. The normalized page-shell manifest is pinned separately and must match the selected candidate exactly in the plan and runtime; it is not part of the candidate-distinctness signature. Ordered regional implementationHash and renderManifestHash values form the other two. Content, media, palette, schema-default, or binding changes cannot make unchanged regional topology a distinct direction. When the prepared strategy is design_system_extension, every candidate must map at least one visible region to new_recipe; the selected region must later resolve to the same-context published recipe at its section root.

Every candidate also supplies an exact pageShell with its root layout and bounded page designTokens layer or null. Every region supplies exact planned nodes, prop schema, prop bindings, media IDs, capabilities, fallback, baseline integer pixel artifactBounds, and an ordered scenarioArtifactBounds crop for every prepared locale × viewport × theme. Every crop is at least 200 × 120 pixels. Baseline crops remain at least 2% of board area and cover at least 40% of the board; each non-baseline crop is at least 2% / required scenario count of board area. Pairwise overlap across all scenario crops is at most 50% of the smaller crop. The server fingerprints each baseline crop and later pixel-compares every scenario crop. Page-scoped tokens must be null outside global page scope. Every candidate pair must have at least one materially distinct matching baseline region crop, so board chrome alone cannot pass. Clients never submit the derived compositionHash, implementationHash, renderManifestHash, page-shell hashes, visual fingerprints, or runtimeHash; registration computes them from the durable artifacts, prepared token snapshot, exact manifests, finalized media ledger, locale resolutions, canonical renderer contract, and deployment fingerprint. Production resolution fails closed without an immutable fingerprint from NEXT_DEPLOYMENT_ID, resolved gitCommitSha such as DEPLOYMENT_GIT_COMMIT_SHA, or resolved deploymentId; "local" is a non-production fallback only. The deployment fingerprint and renderer contract version enter runtimeHash, so a renderer build change invalidates earlier review. The current exact boundary is page renderer contract 7 and prototype runtime contract 4.

Registration returns signed canonical runtime preview URLs plus the exact evidence-upload contract for every A/B/C candidate and locale. Renew an expired URL with yayaw_cms_prototype_runtime_previews_get only while its stored runtime remains current. Capture every prepared locale at desktop/mobile and light/dark, then finalize every screenshot as a distinct image with the returned designContextId, designPlanHash equal to the candidate runtimeHash, and exact visualRole. Desktop evidence is at least 1024 pixels wide and mobile evidence is 240–767 pixels wide. Every finalized screenshot also carries the version-1 capture attestation from its scenario contract. It binds the exact artifact/file hash, runtime hash, scenario ID, observed document height, document end, and ordered region IDs. The region ledger is one unique contiguous vertical partition from 0 through the image/document height, with bands of at least 24 pixels and no gap or overlap. The image height must equal the attested document height and stay below the returned maximum. Scenario assets and file hashes must be unique.

Submit that complete matrix through yayaw_cms_prototype_runtime_review_complete with boardMatchesRuntime: true, all eight fidelity fields passing, and no blockers or unapproved deviations. reviewerRole: "visual_qa" is a logical workflow label tied to the current actor, not proof of an independently authenticated reviewer. The board and canonical runtime are separate artifacts that the reviewer compares visually. Fingerprints, manifests, hashes, durable asset provenance, and capture bindings detect substitution, truncation, replay, and drift. The explicit client_observed_server_validated trust boundary does not claim cryptographic proof of browser execution.

Review completion independently crops and compares every required locale × desktop/mobile × light/dark runtime band against the matching immutable scenarioArtifactBounds crop. The versioned server profile includes multi-scale topology and structure as well as edge, palette, aspect, and blankness metrics, requires one-to-one coverage of every declared reference, and rejects structurally flattened, repeated, omitted, duplicate, foreign, or grossly drifted evidence even when client pass fields are true. The current cms-server-visual-comparison-v3 profile uses contained 192 × 192 normalization. Its maximum distances are 0.06 overall, 0.12 aspect, 0.14 edge, 0.12 luma, 0.12 palette, and 0.08 topology, which makes removal of a signature wash or texture blocking.

Only a current runtime-review receipt unlocks yayaw_cms_prototype_select. For exact-v2, the first call supplies the design context and reason but no candidate or decision note. The operation presents the canonical ordered A/B/C candidates and fresh signed runtime previews. Clients with MCP form elicitation commit the accepted enum response in that call; classic clients receive input_required and resume with the returned bounded challengeId plus prototypeId. A caller-authored decisionNote is rejected. The server-generated selection receipt binds the presentation, challenge, response source, candidate/runtime hashes, and runtime-review evidence. Plan validation and publication recompute the receipt and reject missing, legacy-only, foreign-context, conflicting, or tampered proof while allowing an idempotent retry of the same accepted response. resumable_client is an actor-bound client attestation, not cryptographic proof of a human click. Form elicitation is stronger in-protocol interaction evidence but remains client-mediated. Preview issuance, review completion, selection, planning, page review, and publication re-resolve the candidate and its screenshot ledger. A renderer/deployment, recipe, media, screenshot, or token change makes the receipt stale. Replace and re-register the complete set before selection when runtime drift occurs. A design-token change after v2 registration requires a new prepared context and three fresh prototypes; inventory refresh cannot rebase the gate.

After user selection, import, compile, and publish selected new_recipe components with the same designContextId, and finalize every additional page-media asset that the plan will bind. Then perform the same-token yayaw_cms_design_inventory_refresh; it preserves the v2 gate, runtime-review receipt, selection, and exact prototype designBasis while admitting the exact published revisions and bound media. Submit a strict page plan with version: 2, explicit page.layout, and the returned prototype basis to yayaw_cms_page_design or yayaw_cms_page_rework. Any component declared as new must be used by exact published revision as a new composition section's root component. The plan's layout and page-scoped token layer must reproduce the selected candidate's exact pageShell.

The prepare result includes expiresAt. During a long prototype, import, correction, or review pause, call yayaw_cms_design_context_renew before that deadline. Renewal preserves the complete context and is capped at two hours from original session creation; expired contexts cannot be resurrected. Design-bound component imports include the active designContextId. The server accepts them only after explicit prototype selection and records that provenance on the immutable compiled recipe revision. Mutable registry metadata cannot prove provenance. The extension gate rejects components that existed in the initial baseline, were created by another context, or are referenced through a different published revision.

During a rolling upgrade, reader-compatible servers understand legacy prototypeSet.kind: "generated_set" contexts and v2 prototypeGateV2.kind: "generated_set_v2" contexts, and fail closed if both fields coexist. The separate field preserves format discrimination but does not make a mixed fleet safe: a pre-reader instance can still operate on legacy state and write or select it. Production uses two explicit writer switches. Deploy the reader build with CMS_LEGACY_PROTOTYPE_WRITES_ENABLED=true and CMS_PROTOTYPE_RUNTIME_V2_WRITES_ENABLED=false, drain every pre-reader instance, and establish that build as the rollback floor. Configure and verify private staged uploads, then set both writer switches to false, restart, and drain the legacy writers. Activate exact mutations only with legacy false and v2 true; both true fail closed. For a development renderer upgrade, first use the manual runtime workflow to freeze the current SHA, then re-run the target PR deployment. Its trusted-main resolver preserves freeze only when Coolify configuration and every live app/worker already prove it, deploys the new immutable PR SHA under the shared lock, and proves that SHA plus freeze before the manual exact run consumes the next receipt. It cannot initiate freeze or exact. For a production renderer upgrade, first freeze every live container, then dispatch the rolling workflow with the target full main-history SHA and freeze-upgrade:<sha> confirmation; it refuses to build unless the fleet is live-frozen, performs migrations and both rollouts under that freeze, and proves the new images and SHA before returning. Run the manual exact workflow on the same SHA to create and consume the next receipt. Receipt consumption advances the current rollout operation while the first floor operation remains immutable. Existing exact contexts can be updated normally; only adding a new exact gate requires the transaction-local current operation. Once the floor is active, never roll back below it or re-enable legacy writes. Existing legacy contexts are readable and cancellable only. Assets backed by any legacy direct-upload grant must be re-uploaded through private staging before they can serve as exact prototype or review evidence. A disabled writer returns cms_design_prototype_runtime_rollout_pending. Local and other non-production environments default v2 writes to enabled when the variable is absent.

The exact evidence guarantee ends at publication time. Published pages are not automatically re-captured after a later behavioral renderer change. Until a promotion gate inventories published renderer contracts and supports versioned backward-compatible rendering or migration plus visual re-review, such renderer changes are release-blocking. The shared versions live in cms-renderer-contract.ts; bumping one records the boundary but is not a substitute for the missing promotion safety.

Use dryRun=true before persistence, then call yayaw_cms_page_review to obtain static diagnostics, a resolved runtime hash, a browser evidence contract, and short-lived immutable previews. review_ready is deterministic/static readiness, not visual approval. A Visual QA pass must return an explicit PASS for the exact immutable revision and runtime before the client uploads any durable browser evidence. The first immutable draft and every immutable correction require a new pass. Prefer a separate reviewer when available, but the persisted role does not prove a separate person or credential. Complete every locale × desktop/mobile × light/dark scenario, reduced-motion check, and section-detail check, then submit the evidence through yayaw_cms_page_review_complete. Prototype evidence must repeat the exact artifact identity, include a prototypeRegionRef and all eight fidelity checks for every section, and leave unapprovedDeviations empty. Dry-run and review each revalidate that selected artifact as an image from the same context with the recorded file hash and minimum dimensions. Plan sections must map one-to-one, in order, to the selected prototype's ordered regions as <prototypeId>#<regionId>; swapped, invented, or omitted references fail. Every scenario and reduced-motion evidenceRef is the ID of a durable image uploaded with the exact design context, plan hash, and required visual-review:* role. The exact role prefix returned by review binds the capture to the current page revision and runtime hash. Every scenario and reduced-motion capture also includes the returned version-1 attestation with its exact artifact/hash, runtime, scenario, document height, and contiguous ordered section bands. Completion verifies each capture's static bytes, image kind, design context, plan hash, file hash, viewport dimensions, role, and attestation and binds those fields into the receipt artifact ledger. Page publication re-downloads and re-inspects that ledger plus all prototype boards/runtime captures, and rejects removed, changed, animated, truncated, replayed, reclassified, or cross-context captures as stale visual QA. Corrected plans remain bound to the first contextual page, path/slug, and stable section IDs/slugs. Page and section revisions, session draft binding, and media plan hashes persist in one serializable transaction; any failure rolls back the whole correction. Registry rows carry a durable revision-head pointer advanced by the database on immutable revision insertion, rather than deriving the head from timestamp ordering. Manual or external revisions block this shortcut. An A → B → A correction that would reuse an older matching immutable revision returns an explicit revision-reuse conflict instead of reporting the old revision as a newly persisted head. Contextual page publication rejects a missing or stale receipt, including when a published section dependency changed after review. These tools create drafts only; use publish tools only after completed visual QA and an explicit publication request. Direct page create/save tools require control-plane:admin and CMS_LOW_LEVEL_PAGE_ADMIN_BYPASS_ENABLED=true. The deprecated legacy design tool is also admin-only and disabled unless CMS_LEGACY_PAGE_DESIGN_ADMIN_BYPASS_ENABLED=true.

The blocker loop never skips a revision: dry-run and persist, obtain review_ready, then run Visual QA against that exact revision/runtime. A static/runtime diagnostic, failed scenario, failed fidelity field, blocking issue, or unapproved deviation forbids visual-review:* evidence upload, review completion, and publication. Correct the same contextual plan, persist a new immutable revision, and restart from dry-run. Only a zero-blocker PASS unlocks evidence upload. Any correction or dependency change invalidates the pass, captures, runtime hash, and receipt.

CMS literal values use bounded JSON recursively. Arrays, objects, primitives, and null can be passed directly through component props and section/page bindings; recursive valueShape contracts validate nested values. Legacy stringified JSON values remain readable for existing content.

Dynamic data model tools are the control plane for project-specific native Postgres tables. They store versioned model definitions, validate field and index contracts, publish approved revisions, and deploy generated additive DDL with yayaw_dynamic_data_model_deploy. MCP callers never submit arbitrary SQL: the server builds quoted create table, alter table add column if not exists, and create index if not exists statements from the published definition. The deploy tool requires control-plane:admin, dynamic-data:manage, reason, and supports dryRun to inspect the generated plan before applying it. yayaw_dynamic_data_model_revisions lists immutable revisions with latest, published, and deployed markers, plus optional full documents for MCP clients that need to compare definitions before publishing, deploying, or restoring a previous contract. yayaw_dynamic_data_model_revision_diff compares two immutable revisions, defaulting from deployed to latest, and returns section summaries, changed paths, optional before/after values or full documents, and the same deployment evolution metadata used by the deploy planner. yayaw_dynamic_data_model_revision_restore restores a previous immutable revision as the latest draft only; operators must explicitly publish and deploy that restored draft after reviewing diagnostics and diffs. The dashboard model detail uses the same plan builder for managers, so operators can review the exact SQL, table name, indexes, diagnostics, statement hash, and schema evolution report before applying generated DDL. The deployment plan and deploy responses include evolution.status, evolution.canDeploy, field change entries, and counters for added, retained, removed, renamed, type-changed, and blocking fields. Agents should treat blocked as a signal to create a separate data migration plan instead of attempting destructive DDL through dynamic data.

Model retirement is an explicit two-step lifecycle. First call yayaw_dynamic_data_model_archive with the current latest revision id, a reason, and dryRun=true, then repeat with dryRun=false. Archiving removes the model from generated dashboard navigation, runtime manifests, and data-plane route resolution while preserving its native table, publication, deployment, and immutable revision history. Publishing a valid revision again reactivates an archived model.

Permanent removal uses yayaw_dynamic_data_model_delete only after archival. Always run it with dryRun=true first. The response reports canDelete, native table existence, record count, and every deployed model dependency with its definition path. A real deletion requires confirmModelSlug to exactly match modelSlug; it is blocked while another deployed model references the target or while the native table contains records. allowDataLoss=true bypasses only the record-count guard after explicit operator review, never dependency or archive checks. A successful deletion drops the generated native table and removes the registry row, revision, publication, and deployment history in one Postgres transaction. Durable runtime and control-plane audit events remain as historical evidence.

For related models, archive all targets first, then delete leaf models before the models they reference. This ordering lets dependency checks protect the remaining graph without requiring product-specific deletion logic.

Use yayaw_dynamic_data_bundle_apply for product extension manifests that need multiple models installed together. It accepts a bounded list of model definitions in one global or organization scope, checks dynamic-data:manage for every target model, saves each draft through the same validator, and can optionally publish and deploy the resulting revisions in order. The bundle tool also validates dynamic-model reference fields across the full bundle before any draft is saved: relation.modelSlug must point to another bundled model or to an existing readable model in the resolved target scope, and declared displayField or valueField values must exist on the target model or be safe record system columns. Runtime metricRollups, recordEffects, recordGuards, and relatedRecordFilters must also target bundled or existing readable models; every field they write or filter on must exist on the target model before the bundle can be saved. Declarative ui.detail.relatedRecords panels are validated the same way: their target model must be bundled or already readable in the resolved scope, and their targetField plus any sort fields must exist on that target model or be safe record system columns. It requires control-plane:admin because deploy=true can apply generated native table DDL; use dryRun=true first to validate the manifest without writing. When deploy=true succeeds outside dry-run, the response includes manifest.models for the deployed bundle models so MCP clients can immediately inspect their fields, system columns, runtime routes, and dashboard UI without a separate manifest lookup. Each deployed bundle model also returns deploy.evolution, so batch installers can stop on blocked non-additive changes with the same field-level report as the single-model deployment plan tool. Every bundle response also includes postInstall, a dashboard-safe MCP handoff guide. When deployed, postInstall.models lists each installed model's canonical dashboard route, record-route template, extension metadata, runtime routes with yayaw_dynamic_data_runtime_execute input blueprints, realtime topics with yayaw_dynamic_data_runtime_events input blueprints plus JSON/SSE data-plane handoffs, and record tool blueprints for query/get/create/update/delete smoke checks. Each model's ui.surfaces inventory lists the generated navigation, table, form, and detail surfaces with dashboard routes, ordered table columns, form fields, form sections, display modes, row/detail action IDs, related-record panels, metric cards, event timelines, and summary item IDs. Each runtime route also includes a dataPlane handoff with the generic HTTP endpoint under /api/dynamic-data/runtime/*, required query parameters, the expected dynamic-data-runtime API-key action, the underlying Yayaw authorization action, route rate-limit policy, and whether the route requires a reason. The guide never exposes physical table names and lets an MCP client continue from installation to product-specific verification without hardcoding Yayaw internals. When deployment ran, postInstall.verification adds an ordered checklist that starts with a manifest refresh and then points the MCP client at the generated dashboard route, record query/create dry-run, runtime route dry-runs, and runtime event reads for each installed model. The yayaw_dynamic_data_ui_capabilities response exposes the supported checklist kinds, status values, recommended order, and smoke-test tool names under bundle.postInstall.verification. Every bundle response also includes a summary object plus per-model models[].summary data. These summaries are available in dry-run and real apply modes and report created versus updated models, ready versus blocked models, runtime route counts, UI surface counts, field/table/form/action counts, diagnostic severity counts, and whether publish/deploy steps would run. MCP clients should use this impact summary as the fast smoke check before asking an operator to approve a product manifest. Dry-run bundle validation can omit reason; real bundle writes still require one for auditability.

Device, agent, remote-control, workflow, or analytics control planes should be modeled as organization-scoped dynamic data extensions, not as product-specific backend forks. A manifest can define principal registries, resource registries with server-owned computed status projections from heartbeat/workflow inputs, runtime or endpoint child models, trust bindings, short-lived hashed-code pairing flows, principal-scoped grants, lifecycle records, short-retention raw metric samples, minute/hour/day metric rollups, and audit events. Those models should use normal yayaw_dynamic_data_bundle_apply documents with extension metadata, runtime.api, runtime.realtime, runtime.projections, ui.nav, and Yayaw Table ui.table/ui.form settings.

MCP clients own product-specific bundle documents and can dry-run, publish, and deploy them through yayaw_dynamic_data_bundle_apply. The Yayaw repository must not ship product-specific manifests or wrapper scripts: it exposes the generic model, runtime, UI, authz, and audit contracts that those MCP-supplied manifests use. Hot product callers should use /api/dynamic-data/runtime/* or future dedicated data-plane endpoints for heartbeats, pairing claims, session status changes, and metrics ingestion instead of routing high-frequency traffic through MCP.

For example, a resource registry can expose selected-record dashboard heartbeats and an idempotent external heartbeat route such as /devices/heartbeat; callers send a stable slug plus registry fields, and the generic heartbeat executor creates or updates the native-table record. List reads can use relatedRecordFilters so /devices only returns resources with an active, non-revoked, unexpired grant for the current runtime principal. Endpoint or trust-binding child models can use the same generic heartbeat contract with composite idempotency keys such as resource_id + endpoint_kind + host + port or resource_id + hostname + fingerprint. Status and revocation routes can use patch_status, preconditions, and server-owned value assignments so dashboard row actions and MCP clients do not send trusted lifecycle timestamps.

Model definitions can also include an extension block for product-level ownership, capabilities, and entity bindings, a constraints block for generated unique constraints, and a runtime block for the intended data-plane surface. extension.entityBindings lets a dynamic model declare that it represents, extends, or references a long-lived product entity such as organization, user, principal, resource, workflow, or any product-specific slug. organization is the canonical Yayaw team/workspace target; manifests and discovery calls may use team as an alias and Yayaw normalizes it to organization. extends and references bindings must point to a declared field, while represents can describe the model itself. These bindings are metadata for MCP, dashboard navigation, runtime orchestration, and extension discovery; they never grant access and do not replace Yayaw can(...) authorization. The dashboard no-code model builder can author the same extension key, label, description, version, capabilities, tags, and entity bindings for native-table drafts; MCP bundles remain the path for larger multi-model extension manifests. MCP clients can call yayaw_dynamic_data_entity_extensions with a target such as user, organization, team, principal, or any product slug to discover deployed models that represents, extends, or references that target. When targetId and includeRecordCounts=true are supplied, Yayaw counts linked native-table records only after filtering each model through dynamic-data:list; field-based bindings count rows through their declared binding field, while represents bindings count the represented record by id. constraints currently generate safe unique indexes, including partial unique indexes from field predicates such as revoked_at is null, without accepting arbitrary SQL. runtime.api, runtime.realtime, and runtime.projections document the runtime routes, durable realtime event topics, and derived fields that server handlers consume. MCP remains the control plane and should not sit in hot heartbeat, session, metrics, or event-ingestion paths. runtime.projections supports last_seen_status for same-record presence projections: it reads timestamp inputs, applies a configurable staleAfterSeconds threshold with a 120 second default, and resolves the first two values as offline/reachable labels. It also supports record_status for generic status projections such as resources, workflows, tickets, or inventory items: values[0] is the fallback, boolean true inputs and positive numeric inputs resolve to the matching values[inputIndex + 1], and string inputs pass through when they match an allowed value. state uses the same prioritized resolution for workflow, lifecycle, or availability state fields. count sums same-record inputs by treating arrays as their length, finite numbers as their value, true booleans and non-empty scalars as 1, and empty or missing values as 0. on_read projections are applied to returned record values without mutating storage. Projected fields are always removed from caller create/update payloads and are not required from record callers even when the field definition is marked required. on_write projections are materialized during create/update after stripping client-supplied values for the projected field, so the model, not the caller, owns the computed value. yayaw_dynamic_data_runtime_manifest returns the deployed normalized runtime declarations for discovery by future API, realtime, projection, UI, and extension orchestration layers. Manifest entries include normalized fields, fieldKeys, effective runtimeApiRoutes, the resolved ui.dashboardRoute, ui.dashboardRecordRouteTemplate, ui.dashboardSection, systemColumns, and normalized ui.nav, ui.table, ui.form, and ui.runtimeActions configuration so MCP clients can discover backend interfaces without rebuilding Yayaw Table settings from raw model documents. ui.runtimeActions summarizes row/detail actions, their dashboard surfaces, refresh behavior, route key, resolved method/path, record-id and optimistic-lock behavior, and literal/path binding fields. runtimeApiRoutes filters disabled routes and resolves defaults such as method, path, authz action, idempotency, rate limits, filters, assignments, and preconditions for the model. The no-code model builder can author bounded runtime route declarations with base path, kind, method, path, authz action, stable key, idempotency fields, accepted client value fields, parameter-to-field mapping, field-level preconditions, client value conditions, route record filters, route value assignments, route value increments, route value transforms, rate limits, and reason requirements before publish/deploy. Idempotency fields and accepted client value fields use structured draft-field pickers; client value conditions use bounded field, operator, and literal value controls; parameter-to-field mapping uses bounded parameter binding rows; and preconditions use bounded field, operator, literal value, or now controls, route record filters use bounded field, operator, literal, path parameter, organization, or actor source controls, while route value assignments can set model fields from a literal, now, a path parameter, organization id, or actor identity, including route-wide, create-specific, and update-specific assignment lists. MCP input may still send the same values as arrays, objects, or comma-separated strings. Route value increments can atomically adjust numeric fields with optional min/max bounds. Route value transforms can hash value fields or path parameters into target fields with SHA-256 or HMAC SHA-256, hex or Base64URL encoding, optional trimming, and optional source removal. More advanced route effects, guards, rollups, and cross-model side effects remain available through MCP bundles. The same no-code builder can bind bounded runtime actions to those route keys with label, icon, style, refresh behavior, row record ID inclusion, optimistic locking, an optional audit reason, and optional record-detail placement; advanced conditions, literal values, path params, and value bindings remain MCP bundle concerns. The same dashboard surface can reopen a manageable model as a new draft only when the current document is fully representable by the bounded no-code form; it sends expectedRevisionId for conflict detection and leaves advanced MCP-only documents under MCP control until matching visual controls exist. A route can declare a stable key (or the manifest aliases routeKey or id), which is normalized and returned with the route so MCP clients and hot callers can target a specific deployed route without relying on manifest array order. yayaw_dynamic_data_runtime_routes uses the same manifest route entries and adds cross-model conflict reporting before a generic runtime endpoint is enabled. Route entries include a dataPlane handoff with the HTTP endpoint template, required query parameters, runtime API-key action, Yayaw authorization action, rate-limit policy, and reason requirement, so MCP clients can discover already-installed hot routes without replaying bundle installation. yayaw_dynamic_data_runtime_route_resolve matches a method and path against the deployed route declarations and returns path parameters for dry-run MCP tests. The manifest, route-list, resolve, and execute tools accept routeKey as an optional filter when multiple generic routes intentionally share a method/path shape. yayaw_dynamic_data_runtime_execute executes supported record runtime routes through the generic record services after resolving the route, checking Yayaw dynamic-data authz, and requiring reason for real MCP mutations. Its MCP response includes the effective authz action and any route conflicts observed during resolution alongside the execution result. The shared runtime executor only requires a reason when the matched route declares reasonRequired, so future hot data-plane endpoints can execute heartbeat and metrics routes without inheriting MCP audit friction. The executor currently supports list, get, create, update, patch_status, delete, heartbeat, metrics_ingest, and pairing_claim. heartbeat and metrics_ingest can upsert when the route declares idempotencyKeyField or a composite idempotencyKeyFields list; otherwise metrics ingestion creates a record. pairing_claim requires idempotency key fields, finds exactly one existing record through those fields, then updates it with the merged route params and request values. Composite keys are useful for runtime child models keyed by stable fields such as resource_id + hostname. metrics_ingest routes can also declare metricRollups for native-table aggregates beside the raw sample model. Each rollup targets another dynamic model, chooses a minute, hour, or day window, writes a normalized bucket field, copies declared dimensions such as workflow_id or resource_id, and can increment a countField plus count, sum, min, max, avg, or last value fields. Rollups run after route params, value transforms, and server-owned assignments have produced the normalized sample values, so MCP can bind path params such as :workflowId once and reuse the resulting field for both raw samples and aggregate dimensions. Rollup target models are resolved through the same deployed dynamic-data record services and stay in the source route scope unless targetScope is declared. Bundle apply validates each rollup target model and every written aggregate field before saving drafts, so MCP clients can install raw sample and aggregate models together with a single dry-runable manifest. The dashboard route builder can configure rollup targets, windows, bucket/count fields, dimensions, and aggregate values for metrics_ingest routes. Rollup execution is reported in the runtime result and metric event payload, while raw ingestion remains the durable source of truth for debugging and short-retention analysis. Raw sample models can declare runtime.retention with maxAgeSeconds, an optional timestampField defaulting to createdAt, and an optional pruneBatchSize capped at 1,000. Trusted MCP operators can call yayaw_dynamic_data_retention_prune with dryRun first, a reason, and an optional lower limit; the tool checks Yayaw dynamic-data:manage, resolves the deployed model policy, queries expired records through the generic record service, and deletes each expired row through the same record delete path so reference policies still apply. This gives hot data planes a bounded raw-metric debug table while keeping minute/hour/day rollup models available for longer analytics windows. Routes can declare recordFilters: server-owned filters prepended to caller-provided filters on list/get reads and mutation lookups. They support bounded comparison/null operators, including in and not_in for non-empty literal arrays and is_null_or_greater_than for nullable validity timestamps, plus literal value or the sources now, actor_user_id, actor_api_key_id, actor_principal, organization_id, and param. Use record filters to scope generic runtime routes to records such as the current principal's grants, resources owned by a principal, organization rows, or route-param-owned resources. They complement Yayaw dynamic-data authz; they do not replace server-side authorization. Routes can also declare relatedRecordFilters: server-owned cross-model existence filters applied to the source record query itself. A filter declares a sourceField, a target model and targetField, then optional target filters; the runtime compiles this to a Postgres exists condition so list pagination and totalCount remain correct. Target filters support the same sources as record guards/effects, including actor_principal, param, source: "value", and source: "record". Use related-record filters for read-time contracts such as "only list resources that have an active grant for the current principal" without adding product-specific query handlers. Targets stay in the source route scope unless targetScope is declared, and bundle apply validates each target model plus targetField and target filter fields before saving drafts. The dashboard route builder can configure the basic source field, target model slug, target field, target scope, and existence mode for these filters; nested target filters remain available through MCP manifests for advanced contracts. Routes can also declare recordGuards: read-only cross-model existence checks evaluated before the runtime route reads or mutates records. Guard filters support the normal record filter sources plus source: "value" with sourceField, which reads a normalized value from the route payload, and source: "record" with sourceField, which reads from the existing source record on update-like routes. Use guards for product contracts such as "the current principal must have an active control/admin grant for this resource" without adding a hard-coded backend handler. Guard targets stay in the source route scope unless targetScope is declared, and bundle apply validates each guard target model and filter field before saving drafts. The dashboard route builder can configure guard target models, target scopes, existence mode, and bounded guard filters for the same sources exposed by MCP. Write routes can declare recordEffects for bounded cross-model side effects after the source mutation succeeds. The first supported effect kind is update: it resolves exactly one target record by filters, then applies server-owned valueAssignments and valueIncrements to that target through the same dynamic-data record service. Effect filters support the normal record filter sources plus source: "value" with sourceField, which reads a normalized value from the source mutation payload, such as using resource_id from a workflow claim to update the matching resource row. Update-like routes can instead use source: "record" with sourceField to read immutable or sensitive fields from the existing source record, such as ending a workflow and updating the resource from the stored resource_id without accepting that field from the client. Effects can declare conditions, evaluated against the normalized source mutation payload before the target lookup runs. Use condition operators equals, not_equals, in, not_in, is_empty, and is_not_empty for workflow side effects such as decrementing a resource's active-workflow counter only when the incoming workflow status is terminal. Effect targets stay in the source route scope unless targetScope is declared. Organization-scoped effects require an organization context, target writes use the target record's current dataVersion as an optimistic guard, and effect success or failure is reported in the runtime result and mutation event payload. The dashboard route builder can configure update effects with target model/scope, bounded target filters, server-owned assignments, and numeric increments; MCP manifests remain the best path for large generated bundles. Bundle apply validates each effect target model, filter field, and written target field before saving drafts, so MCP clients can install related product models and their runtime side effects together in one dry-runable manifest. Write routes can also declare preconditions: bounded field filters that must match the target record before the mutation runs. They support equals, not_equals, greater_than, greater_or_equal, less_than, less_or_equal, in, not_in, is_null, and is_not_null; comparison operators accept a literal value or valueSource: "now" for timestamp guards such as expires_at > now, while set operators require a non-empty literal value array. Generic update, delete, heartbeat, metrics, and pairing_claim routes all evaluate the same precondition contract through the record service, so product extensions can model pending-only claims, non-revoked grants, unexpired sessions, or workflow transitions without adding hard-coded backend handlers. Write routes can declare clientValueFields to allow only selected raw client payload fields before params, transforms, assignments, increments, guards, and record validation run. An omitted clientValueFields list preserves the legacy "accept model fields" behavior, while an empty list accepts no raw client fields. The dashboard builder presents that list as field checkboxes instead of free-form comma text. Declared valueTransforms with sourceType: "value" implicitly accept their source field as a transient input, so routes can hash a short code without persisting or separately allowlisting the clear value. Use this for narrow workflow endpoints such as /status, revoke routes, heartbeat payloads, and metrics ingest contracts. Routes can also declare clientValueConditions with the same bounded condition operators as assignments and effects. These conditions are evaluated against the normalized client payload after allowlisting and transforms, but before server-owned assignments. If they do not match, the route fails before any lookup or write; use them to constrain lifecycle inputs such as allowing a workflow status endpoint to accept only active, ended, or failed. Write routes can declare valueAssignments for server-owned field values that are applied after paramFieldMap and valueTransforms, overriding any client payload value for the same declared field. Assignment sources are literal, now, actor_user_id, actor_api_key_id, actor_principal, organization_id, and param. actor_principal stores the generic principal reference format used by dynamic data references, preferring principal:api_key:<id> for API key actors and falling back to principal:user:<id> for session or OAuth actors. Use assignments for fields such as claimed_at, claimed_by_principal_id, heartbeat timestamps, workflow states, and organization-owned route values that must not be trusted from the caller. source: "now" assignments can declare a bounded offsetSeconds, such as 300 for a five-minute expires_at, so TTLs for pairing sessions, temporary grants, or expiring endpoints stay server-owned. Assignments can also declare conditions, using the same bounded condition operators as record effects, so a route can keep the caller-provided workflow state while assigning server-owned companion fields only for specific incoming values, such as setting ended_at only when status in ["ended", "failed"]. Use valueAssignments for values that should apply on every write branch. Routes that can split into create/update branches can also declare createValueAssignments and updateValueAssignments: create-style routes, append-only metrics, and the create branch of heartbeat/metrics upserts apply the create list, while update routes, pairing_claim, and the update branch of heartbeat/metrics upserts apply the update list. This is useful for values such as trust first_seen_at timestamps that must be set once on creation, or server-owned accepted_at/updated_by_principal_id fields that should change only when an existing record is mutated. Write routes can also declare valueIncrements for server-owned numeric counters applied after assignments and before record validation. Each increment targets a declared field, adds by with a default of 1, and can clamp the result with optional min and max bounds. Create-style routes start from zero, while update, heartbeat, metrics upsert, and pairing_claim routes read the current record value first. Use increments for attempt counts, viewer counts, heartbeat counters, or bounded workflow counters without trusting a caller-supplied value. Existing-record increments automatically use the record's current dataVersion as an optimistic write guard unless the caller supplies an explicit expectedDataVersion, so concurrent counter updates fail instead of silently overwriting each other. Metric rollup bucket updates use the same guard against the existing aggregate bucket version. valueTransforms run before idempotency lookup and record validation, so routes can transform params.code or a request value into a declared field such as code_hash without writing the clear source value. Transformed sources are removed from persisted values, response params, event payloads, and route topic params by default; set removeSource: false only when a route intentionally keeps the raw source visible. Supported algorithms are sha256 and hmac_sha256; HMAC transforms require DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRET. paramFieldMap can bind path parameters such as :workflowId to declared model fields before validation. The dashboard builder edits this as structured parameter binding rows instead of free-form param=field text. Both paramFieldMap keys and valueTransforms with sourceType: "param" must reference a path parameter declared by the route's effective path, including default paths resolved from the route kind. Successful non-dry-run mutation routes append a durable event to dynamic_data_runtime_events as a generic outbox before returning. The mapping is create -> created, update -> updated, patch_status -> status_changed, delete -> deleted, heartbeat -> heartbeat, metrics_ingest -> metric, and pairing_claim -> updated; list and get do not emit runtime events. Each event stores scope, organization, model, record, route, event type, payload, actor, and rendered topics from runtime.realtime.channels. Topic templates can use reserved values such as {organizationId}, {modelSlug}, {recordId}, {routeKind}, and {eventType}, plus route params and primitive payload values. Channels with missing tokens are skipped. Event persistence is fail-open for the caller: the record mutation can still succeed with eventId: null while topics remain visible in the response. This outbox is the long-term foundation for SSE, WebSocket, workers, and analytics fan-out. Runtime event consumers can read the generic feed at /api/dynamic-data/runtime-events. The endpoint uses the same session or bearer actor model as runtime execution, requires dynamic-data-runtime:read for dedicated runtime API keys, keeps the legacy control-plane fallback for existing MCP keys, and still checks Yayaw dynamic-data:list authz for the requested scope or model. Trusted MCP clients can inspect the same outbox through yayaw_dynamic_data_runtime_events for debugging and operations, while hot runtime consumers should keep using HTTP/SSE. JSON polling is the default. stream=true or Accept: text/event-stream opens a bounded SSE stream over the outbox, using the event cursor as the SSE id so clients can resume with Last-Event-ID. Filters include scope, orgId, topic, modelId, modelSlug, recordId, routeId, routeKind, eventType, cursor, and limit; SSE also accepts pollMs and durationSeconds. yayaw_dynamic_data_ui_capabilities advertises the endpoint, supported transports, cursor parameter, resume header, and query filters, while each deployed runtime manifest exposes runtimeEventFeeds entries derived from runtime.realtime.channels with ready-to-use JSON and SSE query templates. Clients replace topic-template tokens such as {recordId} or {organizationId} before making the request. The dynamic-data dashboard applies the same recordId filter when a table row is selected. Model definitions can also declare ui.detail.eventTimelines to render named dashboard timelines with eventType, routeKind, topic, topicTemplate, limit, and recordFilter controls, so operators can inspect product-specific resource, workflow, trust, or metrics activity without custom pages. Timelines that declare routeKind must match an enabled runtime route kind, and timelines that declare topicTemplate must match a declared realtime channel topic template so generated dashboard panels do not silently render empty feeds. Topic containment filters are backed by a GIN index on the durable outbox, so channel-scoped feeds stay viable as session and metrics events grow. The current transport polls the durable outbox and emits heartbeat and complete events; future WebSocket or database pub/sub delivery can reuse the same event contract. Hot runtime callers should use the generic data-plane endpoint /api/dynamic-data/runtime/* instead of MCP. For example, POST /api/dynamic-data/runtime/resources/heartbeat resolves the deployed runtime route /resources/heartbeat and executes it through the same generic runtime executor. The endpoint accepts the active dashboard session or a Yayaw bearer actor. API keys should use dynamic-data-runtime:read for read routes and dynamic-data-runtime:write for write routes; existing MCP keys remain compatible through the control-plane fallback described above. Organization actors default to their own organization runtime routes, and the endpoint still checks Yayaw dynamic-data authz before executing anything. Query string scope=global|organization, orgId, extensionKey, and routeKey can select an explicit route scope or disambiguate a stable extension route when the actor is allowed. JSON bodies can pass record values, filters, search, sort, record limits/offsets, optimistic expectedDataVersion, and optional reason for routes that declare reasonRequired. After a bundle deployment, MCP clients do not need to reconstruct those HTTP calls manually: postInstall.models[].routes[].dataPlane exposes the endpoint, query parameters, API-key action, Yayaw authorization action, rate-limit policy, and reason requirement for each deployed runtime route. Read-heavy product clients that need a server-owned, multi-model handoff can use GET /api/dynamic-data/extensions/{extensionKey}/snapshot. The snapshot endpoint reads the deployed runtime manifest for that extension, filters each model through the caller's dynamic-data:list authorization, and returns a bounded set of records per visible model with dashboard links, field keys, runtime-route counts, and record totals. It accepts the same dashboard session or bearer runtime actor as the runtime endpoint; bearer keys need dynamic-data-runtime:read. Product-specific shaping still belongs outside Yayaw, but this endpoint gives MCP-installed extensions a generic server boundary without putting product code in the repository. Realtime channels expose the same handoff through postInstall.models[].realtime[].dataPlane: json is the polling endpoint, sse adds stream=true, and cursorParam plus resumeHeader describe how to resume event reads. Successful HTTP responses expose a public data-plane envelope: result, params, rateLimit, dryRun, runtimeEvent, the effective authz action, route conflicts, and safe route metadata (id, kind, method, path, key, modelSlug, scope, dashboardRoute, dashboardRecordRouteTemplate, and dashboardSection). Internal execution details such as physical table names stay server-side. When a deployed route declares rateLimit.maxRequests and rateLimit.windowSeconds, the data-plane endpoint enforces that limit before execution using the configured subject (actor, ip, organization, principal, or record). Counted responses include RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset; exceeded routes return 429 with Retry-After. Reference fields use the same native-table model contract. A reference field must declare either relation.modelSlug for another dynamic model or relation.targetEntity for a system entity (user, organization, team, api_key, or principal). It can also declare targetScope, displayField, valueField, and onDelete intent for dynamic model references. The generated table stores the reference value as a native text column and indexes reference fields by default so relationship-heavy models such as principals, resources, grants, and workflows remain fast. Record create and update operations resolve deployed dynamic target models and reject missing target records before writing, using id by default or the declared valueField when one is configured. System references currently store ids and validate user, organization, team, and api_key against Yayaw system tables. principal references use stable values such as principal:user:<userId> and principal:api_key:<keyId>; user principals must resolve to a user in the source organization when one is present, and API key principals must resolve to an enabled key for that organization. Other subject types such as resource or agent resolve through deployed dynamic models in the same scope: either a model declares extension.entityBindings with kind: "represents" and target equal to the subject type, then the principal ref id is matched against that model's record id, or a generic model represents principal and stores the subject in conventional type and ref_id fields. The dashboard no-code model draft builder exposes the same reference, composite index, unique-constraint, and per-field UI presentation settings for new models, so operators can create relation-heavy native-table contracts and usable Yayaw Table form/table surfaces without editing raw model JSON. Composite index and unique-constraint field selectors are structured pickers derived from the draft fields while MCP bundles keep using explicit field arrays. The builder includes finite option lists for select, radio, and multiSelect widgets plus value-type field bindings for dynamic primitive editors. Runtime route builders resolve visible target model slugs into selects and derive target-field pickers for metric rollups, related-record filters, record guards, and record effects while preserving unknown existing values for compatibility. targetScope: "same" follows the source model scope for dynamic targets, while explicit organization targets require an organization context. Record deletes apply dynamic reference policies before removing the target: omitted onDelete behaves as restrict, restrict blocks deletion while dependent records exist, and set_null nullifies optional referencing fields while bumping their data version. cascade is intentionally rejected while dependents exist until Yayaw has transactional graph deletion with cycle guards for generated tables. In the dashboard record form, dynamic model reference fields can compile into select inputs when the target model is visible to the actor: valueField provides the stored value, displayField provides the option label, and targetScope: "same" follows the source model scope. System references to user, organization, team, and api_key can also compile into selects from the scoped Yayaw system tables; principal compiles user, API key, dynamic subject models, and dynamic principal registry records into prefixed option values when available. Dynamic principal options follow the same scope as runtime validation: deployed models with extension.entityBindings.kind: "represents" can represent concrete subject types, while a model that represents principal can expose type plus ref_id records. If options cannot be loaded, the field gracefully falls back to the normal text input instead of weakening authorization. MCP clients can call yayaw_dynamic_data_reference_options with a model slug and field key to load the same dashboard-safe options for system references and dynamic-model references, after the source and target model permissions are checked server-side. The dynamic-data dashboard model list also exposes a bounded no-code creation flow for actors with dynamic-data:manage. It creates draft native-table model documents from a name, slug, scope, navigation section, and scalar fields, then hands the draft to the same dynamic-data validation, publish, and deploy pipeline used by MCP before any generated Postgres DDL can run. The same model detail exposes a deployment-plan preview for the published revision and the button that applies that reviewed plan, while the model list row action opens that review instead of applying DDL directly. The preview shows the same schema evolution status and blocking-change counters returned to MCP callers. The detail also shows a revision lifecycle panel with latest, published, and deployed markers, the most recent immutable revisions, and the deployed-to-latest diff summary from the same revision services exposed through MCP. Actors with dynamic-data:manage can restore an older immutable revision from that panel after confirming the target revision and entering a reason; the restored revision becomes the latest draft and still requires explicit publish and deployment review. Those actions reuse the shared dynamic-data services and server-side dynamic-data:manage authorization rather than trusting dashboard state. Admin > Dynamic Data and every generated MCP-declared dynamic-data section root show an operator readiness panel above the model table, summarizing visible models, blocked drafts, publish/deploy backlog, deployed runtime routes, runtime event feeds, metric-ingest routes and rollup counts, UI/navigation coverage, and the entity-extension binding and target counts plus the MCP/runtime handoff endpoints operators should check after applying a bundle. Model definitions can also include a ui block. MCP clients can call yayaw_dynamic_data_ui_capabilities to discover the supported field types, extension capabilities, entity binding kinds, reserved system targets, runtime API route kinds/defaults, runtime authz actions, rate-limit subjects, realtime events, projection kinds, Yayaw Table column renderers, safe aliases, form widgets and widget aliases, reference option sources, extension discovery tools, navigation sections, the static admin control-plane root, the dynamic section route pattern, and table enum values before generating a model document. The same capabilities payload exposes dashboard.operatorReadiness, including the static control-plane root, generated section route pattern, attention states, summarized fields, authz guarantees, and MCP/runtime handoff endpoints rendered by the dashboard readiness panel. MCP bundle apply and post-install responses also surface extension binding counts, bound targets, metric route counts, rollup counts, and metric route keys per model so operators can verify native extension and analytics routing without adding product-specific code. The capability payload includes per-field compatibility defaults for form widgets, table renderers, and inline editors, plus supported display modes, layout presets, row-click modes, column display variants, type-specific column options, URL display modes, inline-edit editors/triggers, gallery presets, declarative row-action styles, refresh modes, binding sources, condition operators, detail summary item types, related-record panel limits, related-record filter operators/value sources, detail action variants, detail event timeline filters, detail metric-card sources, runtime value assignment, filter, effect, and transform sources, target examples for generic subjects such as resources and activities, and the safe record system columns (id, dataVersion, timestamps, actor IDs, and organization ID) that can be used in table config, sorting, filtering, and global search. Model validation reports non-blocking warnings when declared ui.form widgets, ui.table column renderers, or inline editors fall outside those compatibility defaults, so MCP-generated interfaces can stay product-neutral while still surfacing suspicious UI choices before deployment. ui.table configures the dashboard Yayaw Table surface for the deployed records, including column renderer overrides (text, number, boolean, date, select, tag, multiSelect, image, url, code, json, string, and dynamicType) plus safe aliases such as jsonb, badge, option, multiOption, currency, decimal, integer, and dynamic-type. It also controls visible columns, column order, default sorting, number/date formatting, tag colors, JSON preview item counts through columns.<field>.maxItems, string quote display through columns.<field>.showQuotes, footer calculations, saved-view behavior, inline edit, empty states, global-search participation through columns.<field>.enableGlobalSearch, and table/kanban/gallery display settings. ui.table.order, ui.table.visible, and ui.table.mandatory may also include Yayaw Table's virtual select and actions columns so MCP clients can place row selection and action menus without modeling them as database fields; field-backed config such as sort, kanban, gallery, and ui.table.columns still references declared fields or safe record system columns only. Listing select in one of those display lists enables row selection automatically unless ui.table.enableRowSelection is explicitly false. The dashboard no-code builder can configure bounded table view metadata for those modes, including row selection mode, available display modes, the default mode, kanban grouping/title/card columns, and gallery image/title/card presentation without editing the raw model document. Its row selection control can keep MCP virtual-column inference in auto, or persist an explicit Yayaw Table enableRowSelection value. Table display modes, kanban/gallery field references, and index or unique-constraint field lists render as structured selects or multi-selects derived from the current draft fields, while MCP input may still send the same values as arrays or comma-separated strings. It can also edit bounded column-level Yayaw Table options for header overrides, sorting, filters, global search, grouping, sizing, default calculations, date display presets, number formats, URL display modes, JSON preview limits, tag color maps, string quote display, and dynamic-type keys. The same builder can author bounded runtime.realtime.channels and ui.detail.eventTimelines, validating timeline route kinds against enabled runtime routes and topic templates against declared realtime channels before saving the draft. Realtime channel event filters render as structured event checkboxes while MCP bundles may still send event arrays or comma-separated strings. ui.table.rowActions can add model-specific row menu actions without repository code: each action declares a label, optional icon/style, optional enabledWhen conditions against the source row, and a runtime target with a stable routeKey, optional method/path override, literal values, valueBindings, pathParams, optional optimistic locking, and refresh behavior. New MCP-generated interfaces should set runtime.routeKey to the matching runtime.api.routes[].key so row actions stay stable if route order changes; the dashboard no-code builder writes the same stable route-key contract for bounded actions and can optionally surface those actions on the record detail view without requiring repository code. method/path remain a fallback resolver and path-parameter source. When a row action declares method/path directly, or overrides method/path beside a routeKey, the effective target must match an enabled runtime route after runtime.api.basePath resolution. The dashboard re-reads the source record server-side, resolves literal, record, and system bindings such as recordId, dataVersion, or organizationId, checks Yayaw dynamic-data authz on the matched runtime route, then executes the existing dynamic runtime service; UI actions never grant permissions by themselves. The model contract panel in the dashboard exposes each row action's runtime route key, method/path, refresh mode, literal values, path params, value bindings, conditions, record-id usage, and optimistic-lock requirement so MCP-generated interfaces can be audited without reading the raw model document. yayaw_dynamic_data_ui_capabilities exposes the same row-action execution contract in machine-readable form, including default refresh behavior, server checks, target resolution order, method/path fallback support, path params, literal values, value bindings, enabledWhen, optimistic locking, and route-key support. ui.detail.actions can surface selected-record buttons by referencing existing ui.table.rowActions through actionId. Detail actions can override label, icon, and button variant, inherit the row action style when no variant is declared, honor the same enabledWhen conditions, and execute through the same server-side runtime/authz path as the table menu. The contract panel also lists detail buttons and the row action they bind to. ui.detail.metricCards can add compact KPI cards to the selected-record detail header. Cards can read safe literal, record, and system bindings, or read metric: "totalCount" from an existing ui.detail.relatedRecords panel with source: "relatedRecords" and panelId. Related-record metric cards reuse the already authorized related-panel query result; they do not add new database reads or bypass target model authorization. ui.detail.eventTimelines can add named runtime-event panels to the model detail column. Each timeline can filter by eventType, routeKind, routeId, topic, or topicTemplate, choose a bounded limit, and use recordFilter: "selected", "active", or "none" to decide whether the selected row is required, applied opportunistically, or ignored. routeKind references must match an enabled runtime route kind, and topicTemplate references must match a declared realtime channel topic template. The dashboard no-code builder can configure these timeline panels alongside realtime channels for operators who need live record activity without editing JSON directly. ui.detail.summary configures the selected-record summary shown above related panels. It can declare title, description, badges, and fields using the same safe literal, record, and system bindings as row actions. Summary items support bounded display hints (text, number, boolean, date, tag, code, and url) and never fetch data beyond the record already authorized for the dashboard table. ui.detail.relatedRecords configures read-only detail panels for records related to the selected dashboard row. Each panel declares an id, label, target model slug, target field, optional target scope (same, global, or organization), optional limit, optional bounded sort, optional bounded filters, optional rowClickMode (none or activate), and a safe source binding. The dashboard reloads the source record server-side, resolves the source value, checks dynamic-data:list or dynamic-data:manage on the target model, and queries the target native table with a parameterized equality filter plus the declared sort and filters. Related-record filters are ANDed with the relation filter, use safe target fields or record system columns, support the normal runtime comparison/null operators, and can compare against literal values or source: "now" for time-window panels such as active sessions or non-expired grants. When rowClickMode is activate, clicking a related row opens the target model's canonical dashboard route with recordId in the URL so the target record summary, runtime events, and related panels hydrate after reload or cross-section navigation. This lets MCP clients compose operational interfaces such as resource runtime, endpoint, workflow, grant, or metric panels without adding product React code. Native dashboard pages can also surface MCP-authored extensions without adding product-specific React code. The shared dashboard entity-extension loader discovers deployed models whose extension.entityBindings target a native entity such as organization, queries only the global scope and the actor's active organization scope, filters every model through dynamic-data:list or dynamic-data:manage before any record counts are loaded, and returns the canonical model route, binding field, extension metadata, and authorized count. Organization settings uses the reusable entity-extension panel for the active organization target, and account settings uses the same panel for the current user target. Those server-rendered panels stay hidden when no authorized extension exists so native settings pages remain quiet by default. Admin > Users mounts a client-side version for the selected user detail, and Developer settings mounts the same client panel under each listed API key when a deployed model extends api_key; both client panels also stay hidden when no authorized extension exists. Operators can therefore see dynamic native-table models that extend core native entities and open the model surface from the native page. When the native page has a concrete target id, the panel links to the canonical dynamic-data route with the model's Yayaw Table record filters encoded in the URL for field-based bindings, or with recordId for represents bindings, and the dashboard server preload applies that state before rendering the record table. yayaw_dynamic_data_ui_capabilities exposes these native panel mount points as extension.nativeSurfaces, including the target, dashboard route, scope, and target-id source such as current_user_id, selected_user_id, listed_api_key_id, or active_organization_id, so MCP clients can discover where a represents, extends, or references binding will become visible before applying a bundle. ui.form configures the Yayaw Table create/edit form generated for records: MCP callers can set field order, visibility, sections, labels, descriptions, placeholders, disabled or read-only state, drawer/modal layout, and safe widget hints such as text, textarea, number, select, radio, switch, checkbox, url, email, tel, password, date, json, multiSelect, value-type, and collection. ui.form.sections groups existing fields under titled sections without changing the record payload shape; fields omitted from every section still render after the declared sections. Select and radio fields can receive MCP-provided options, numeric fields can receive min, max, and step, date fields can receive serialized minDate and maxDate, textarea/json fields can receive rows, URL fields can enable metadata preview, and value-type fields must declare a valid valueTypeField so Yayaw Table can select the editor. collection widgets are supported for json model fields through a declarative collection block with item fields, columns, optional create item/actions, item labels, empty labels, and required item-field validation; the dashboard compiles that block into Yayaw Table's native collection editor without accepting custom React renderers from MCP. The no-code model builder can author collection item fields, columns, labels, and defaults for JSON-backed collections without editing the model document directly. multiSelect widgets are also supported for json model fields when the field declares finite string or number options; the dashboard compiles them into the native Yayaw Table checkbox-list form field and stores the selected values as a JSON array. ui.nav adds deployed models to the backend sidebar, section navigation, breadcrumbs, and command menu search when the ui.nav object is present, unless enabled is explicitly false. It uses the configured built-in or MCP-generated dashboard section and links to the canonical dynamic-data record route /dashboard/{section}/dynamic-data/{scope}/{modelSlug} with the target model selected. The dashboard no-code model creator writes the same ui.nav metadata as MCP for enabled, label, icon, order, and section, so operators can promote a quick native-table draft into clean dashboard navigation without touching code. When static section links and dynamic model links both exist, the sidebar separates the generated model links while breadcrumbs, section navigation, and command search expose only navigable destinations. Appending ?recordId=<id> selects a specific deployed native-table record and loads it server-side even when it is not present in the first table page. Admin > Dynamic Data is the static control-plane surface for model management. Product surfaces should be declared by MCP with ui.nav.section; any normalized section outside the built-in set becomes a dashboard app section at /dashboard/{section}/dynamic-data when at least one visible deployed model is assigned to it. Generated app sections prioritize the selected model's Yayaw Table record surface, record detail metadata, related-record panels, and runtime event timelines; the model lifecycle/readiness control-plane remains available from Admin > Dynamic Data. Bundles can set ui.nav.sectionLabel, ui.nav.sectionIcon, and ui.nav.sectionOrder to control the generated top-level section without code changes; set ui.nav.enabled=false only for deployed models that should stay hidden from navigation. Models without a configured section stay in Admin > Dynamic Data, and the older ?model=&scope= query selection remains accepted for compatibility. ui.nav.icon must be one of the nav.icons values returned by yayaw_dynamic_data_ui_capabilities; unsupported icons are ignored so the sidebar can fall back to TableProperties. yayaw_dynamic_data_runtime_manifest also returns normalized fields, effective runtimeApiRoutes, runtimeEventFeeds, systemColumns, ui.dashboardRoute, ui.dashboardRecordRouteTemplate, ui.dashboardSection, ui.table, ui.form, ui.detail, and ui.runtimeActions for each deployed model so MCP clients can link to the resolved dashboard model surface, open a selected record through the recordId={recordId} template, inspect row/detail runtime actions, subscribe to declared JSON/SSE runtime-event feeds, and render configured Yayaw Table interfaces without rebuilding schema, record-system, route, relation, event-feed, or UI rules themselves. The bundle post-install verification checklist includes runtime_ui_action_dry_run steps for actions whose runtime target resolves to a deployed route. The dashboard model contract view summarizes each runtime route's server-owned filters, preconditions, parameter bindings, value transforms, assignments, increments, metric rollups, and record effects, including rollup and effect target model slugs for operator debugging. Deployed route cards also show the generic data-plane endpoint, query parameters, runtime API-key action, Yayaw authorization action, rate-limit policy, and reason requirement from the same manifest. For deployed models, that contract view and the generated record/detail actions read the same normalized runtime manifest that MCP returns, so the dashboard shows the routes and ui.runtimeActions that are actually installed rather than an unpublished draft document. Model-level navigation is filtered with Yayaw authz against dynamic-data:list and the deployed model resourceId, so a broad global or organization grant exposes every visible model in that scope while a resource-scoped grant exposes only the matching model. Canonical model route guards resolve {scope, modelSlug} to the deployed registry item and retry the same dynamic-data:list check with that model resourceId, so direct deep links use the same resource-scoped contract as the sidebar instead of requiring broad dynamic-data access. The dashboard model list uses the same model-level filter, shows extension labels and capabilities as filterable Yayaw Table columns, and per-model dynamic-data:manage controls generated record actions. Navigation filtering preserves the configured section parent when an authorized dynamic model is visible, even if that section has no visible static child route for the current actor. The dynamic-data dashboard model detail also surfaces extension key/label/version, capabilities, tags, entity bindings, the resolved ui.nav label, icon, section, canonical route, deployed runtime API routes, runtime event feeds, realtime topics, projections, and UI surface metadata so MCP-authored product contracts are inspectable in production without opening the raw model JSON. UI references are validated against declared fields and safe record system columns before a revision can be published; UI config never grants data access and does not affect generated Postgres DDL. Yayaw Table's custom form field still requires React renderers and must not be advertised through MCP model documents.

Dynamic data record tools operate only on deployed native tables. Reads require dynamic-data:list or dynamic-data:read; creates, updates, and deletes require control-plane:write, dynamic-data:manage, and reason. Record payloads are validated against the deployed model definition, dashboard record tables send pagination, simple filters, supported Yayaw Table advanced filters, global search, and sorting to Postgres instead of slicing a fixed client-side sample, filters, searches, and sorts can reference only declared fields or safe system columns, and values are always parameterized by the server rather than submitted as SQL. Global search defaults to id plus text-like declared fields, or to the fields explicitly marked with enableGlobalSearch in ui.table.columns. The yayaw_dynamic_data_records_query MCP tool accepts either a search string or { query, fields } for explicit bounded search. Supported advanced filters include text contains/starts/ends/not-contains/empty, number/date equality and ranges, select inclusion/exclusion, and conservative multi-select text containment for JSON-like stored values.

Dynamic data is for operational project records that need native-table performance. Session, page-view, and high-volume event data should continue to flow through the analytics provider ingestion path; MCP should configure or query those systems, not sit in the hot ingestion path.

yayaw_kibo_sections_generate creates or updates CMS-native kibo-ui-block recipes for the tracked Kibo UI block catalog. It never stores arbitrary third-party TSX as executable runtime content: every generated section is a Yayaw recipe section rendered by checked-in Kibo block examples and their vendored Kibo/shadcn dependencies. Generated recipes expose editable text, link, media, and JSON list props so pages can bind localized Yayaw content through PageSection.inputBindings while the Kibo defaults remain the fallback preview. Use dryRun=true to inspect the target slugs and set publish=true only when the caller explicitly wants the validated revisions published.

Page publish/archive, data entry publish, and section publish tools require expectedRevisionId to preserve optimistic locking. Section archive accepts expectedRevisionId when the caller has a known latest revision. Destructive archive tools, including page, section, component, and organization public domain archive, require confirm: true.

Every MCP tool call attempts to write control_plane_audit_events with actor, API key, operation, target, environment, transport, dry-run state, reason, status, error summary, and result metadata. A successful audit insert returns auditId in the tool response; if audit storage is unavailable, the server logs the failure and the response is returned without auditId.

Future Feature Contract

New operational features should be designed as control-plane-capable unless they are intentionally UI-only.

When a feature needs content, configuration, publish, status, or audit workflows, implement it through the shared control-plane pattern:

  • Put business logic in a reusable service entrypoint that accepts an explicit actor/context argument.
  • Keep cookie-session reads in UI adapters, not in the reusable service layer.
  • Have UI server actions, MCP tools, CLI scripts, and optional HTTP APIs call the same permission-checked service logic.
  • Register typed MCP tools/resources in src/lib/server/services/control-plane when MCP clients should operate or inspect the feature.
  • Require reason for writes, dryRun where practical, expectedRevisionId for optimistic publish flows, and confirm: true for destructive actions.
  • Gate API-key access with Better Auth control-plane:* permissions and still run Yayaw resource authorization with can(...) or scoped membership checks.
  • Attempt audit-event recording for all control-plane operations and surface auditId when storage succeeds.
  • Update this documentation and content/llm/llm-source.md, then regenerate assistant files with bun run docs:llm:generate.
  • Add focused tests for schemas, permission mapping, dry-run/write guards, audit insertion, and MCP discovery or tool-call smoke coverage.

If a feature is intentionally excluded from MCP/CLI, document the reason in the feature docs or PR summary.

Validation

Useful checks after control-plane changes:

bun test src/lib/server/services/control-plane/control-plane.test.ts
bun test src/lib/server/services/pages/cms-page-design-validation.test.ts
bun test src/lib/server/services/pages/cms-page-review-evidence.test.ts
bun test src/lib/server/services/control-plane/oauth-metadata.test.ts
bun run mcp:stdio