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-runtimeAPI key resource. - The
control-plane-mcp-enabledsite 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 seedThen 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:adminThis 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.tsFor 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-smokeThe 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,adminThe 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,writeFor a runtime-only data-plane key, leave control-plane permissions empty and grant:
dynamic-data-runtime:read,writeFor 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,writeThe 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,adminIssue an organization-owned MCP API key:
bun run mcp:key -- issue --email owner@example.com --organization-slug acme --permissions read,write,publish,adminList a user's keys and their stored permissions:
bun run mcp:key -- list --email admin@example.comList an organization's keys and their stored permissions:
bun run mcp:key -- list --organization-slug acmeInspect 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_KEYRevoke 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_capabilitiesyayaw_statusyayaw_audit_listyayaw_deploy_status
CMS and pages:
yayaw_pages_list,yayaw_pages_get,yayaw_pages_validate,yayaw_pages_diffyayaw_pages_publish,yayaw_pages_archive,yayaw_pages_deleteyayaw_pages_create_draft,yayaw_pages_save_draft(disabled maintenance bypasses)yayaw_components_list,yayaw_component_get,yayaw_component_import,yayaw_component_recipe_importyayaw_components_sync_registry,yayaw_component_recompileyayaw_component_publish,yayaw_component_deleteyayaw_sections_list,yayaw_sections_get,yayaw_sections_create_draftyayaw_sections_validate,yayaw_sections_save_draftyayaw_sections_publish,yayaw_sections_archiveyayaw_kibo_sections_generateyayaw_cms_design_prepare,yayaw_cms_design_resume,yayaw_cms_design_guidance_get,yayaw_cms_design_context_renew,yayaw_cms_design_inventory_refreshyayaw_cms_prototypes_register,yayaw_cms_prototype_runtime_previews_get,yayaw_cms_prototype_runtime_review_complete,yayaw_cms_prototype_selectyayaw_cms_page_design,yayaw_cms_page_rework,yayaw_cms_page_review,yayaw_cms_page_review_completeyayaw_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_getyayaw_data_entry_upsert,yayaw_data_entry_publishyayaw_dynamic_data_ui_capabilitiesyayaw_dynamic_data_target_readinessyayaw_dynamic_data_reference_optionsyayaw_dynamic_data_models_list,yayaw_dynamic_data_model_getyayaw_dynamic_data_model_revisions,yayaw_dynamic_data_model_revision_diff,yayaw_dynamic_data_model_revision_restoreyayaw_dynamic_data_entity_extensionsyayaw_dynamic_data_runtime_manifestyayaw_dynamic_data_runtime_routesyayaw_dynamic_data_runtime_eventsyayaw_dynamic_data_runtime_route_resolveyayaw_dynamic_data_runtime_executeyayaw_dynamic_data_model_save,yayaw_dynamic_data_model_publishyayaw_dynamic_data_model_deployment_plan,yayaw_dynamic_data_model_deployyayaw_dynamic_data_model_archive,yayaw_dynamic_data_model_deleteyayaw_dynamic_data_bundle_applyyayaw_dynamic_data_records_query,yayaw_dynamic_data_record_getyayaw_dynamic_data_record_create,yayaw_dynamic_data_record_update,yayaw_dynamic_data_record_deleteyayaw_email_templates_list,yayaw_email_template_getyayaw_email_template_save,yayaw_email_templates_sync_systemyayaw_org_domains_list,yayaw_org_domain_add,yayaw_org_domain_checkyayaw_org_domain_set_primary,yayaw_org_domain_archive
Site operations:
yayaw_billing_products_list,yayaw_billing_product_updateyayaw_stripe_discounts_list,yayaw_stripe_discounts_syncyayaw_media_list,yayaw_media_search,yayaw_media_generate,yayaw_media_upload,yayaw_media_import_url,yayaw_media_upload_prepare,yayaw_media_upload_completeyayaw_design_tokens_get,yayaw_design_tokens_saveyayaw_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://statusyayaw://schemasyayaw://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-enabledis 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-planewhen MCP clients should operate or inspect the feature. - Require
reasonfor writes,dryRunwhere practical,expectedRevisionIdfor optimistic publish flows, andconfirm: truefor destructive actions. - Gate API-key access with Better Auth
control-plane:*permissions and still run Yayaw resource authorization withcan(...)or scoped membership checks. - Attempt audit-event recording for all control-plane operations and surface
auditIdwhen storage succeeds. - Update this documentation and
content/llm/llm-source.md, then regenerate assistant files withbun 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