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/mcpAuthorization server metadata:
/.well-known/oauth-authorization-server/api/authOpenID metadata:
/.well-known/openid-configuration/api/authAuthorization server base:
/api/authMCP 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 [email protected] --permissions read,write,publish,adminIssue an organization-owned MCP API key:
bun run mcp:key -- issue --email [email protected] --organization-slug acme --permissions read,write,publish,adminList a user's keys and their stored permissions:
bun run mcp:key -- list --email [email protected]List 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
Authorization groups
yayaw_authz_groups_list lists real authorization groups with optional
organizationSlug and search filters and bounded limit/offset pagination.
An organization filter matches role-binding scope, not a group's name or metadata.
yayaw_authz_group_get accepts groupId, limit, and offset and returns actual
role bindings, policies, and member IDs for inspection.
yayaw_authz_group_members_update accepts groupId, explicit userIds (at most
100), mode (add or remove), expectedGroupVersion, dryRun, and reason.
Inspect the target group and its bindings, preview with dryRun: true, then apply
with dryRun: false. It uses the same service as dashboard group administration.
It changes only membership in that explicit group and never reads or writes
Better Auth user or organization role strings. Other groups remain unchanged.
These tools require the corresponding global authorization resource permissions;
organization-bound credentials cannot operate this platform administration surface.
Reads require control-plane:read; membership changes require
control-plane:admin plus fresh group-membership:manage. Read-only and externally
managed groups cannot be edited; removing managed memberships is skipped. The
normal MCP audit pipeline records previews and failures, and successful mutations
also record an authorization audit event and invalidate affected permission caches.
Product machine visibility and connection rights still need their own correctly
scoped policies; organization membership alone does not grant Kyber access.
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_records_aggregate,yayaw_dynamic_data_record_getyayaw_dynamic_data_record_create,yayaw_dynamic_data_record_update,yayaw_dynamic_data_record_deleteyayaw_table_views_list,yayaw_table_view_get,yayaw_table_view_save(organization-shared saved views of dynamic data tables)yayaw_screens_list,yayaw_screen_get,yayaw_screen_save_draft,yayaw_screen_publish(explicitly requested publication of a stored draft),yayaw_screen_revisions,yayaw_screen_revision_restore,yayaw_screen_create_draft(dashboard screens: list, read and draft, read the published versions and restore one as a draft, an organization's or, withscope: "global", the platform's global screens, and create an organization's custom screen as a draft; publication never changes access)yayaw_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_media_tags_list,yayaw_media_tags_apply(media tags: read them, tag files; the catalog is managed in the dashboard)yayaw_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 key or OAuth token does not come from an administrator impersonating the user.
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.
An administrator impersonating a user never obtains a credential in that user's name, because a credential outlives the impersonation and does not say it came from one. While the browser login is an impersonation, Yayaw refuses:
OAuth authorization, so no assistant or application can be connected (the browser lands on the authorization error page with
access_denied);device approval;
creating, changing or re-keying an OAuth client;
a product's audience token, whether asked with the impersonation's session token or with an OAuth grant it obtained;
creating an API key or changing its permissions.
The token endpoint issues no token for a code or a refresh whose login is an impersonation (invalid_grant). Every OAuth grant still bound to an impersonation login, offline refresh tokens included, is deleted when that login ends.
An MCP OAuth token names the login it was granted from. When that login is an impersonation, the token can still read: every tool that changes something answers impersonation_refused and is left out of the tool list, and audit events record the administrator in metadata.impersonatedBy. Product clients' bearer APIs and the dynamic-data runtime also treat such a token as read-only.
The impersonation's own session token, sent as a bearer, is such a credential too, and it only reads. Better Auth's endpoints answer its reads and refuse every change with YAYAW_IMPERSONATION_BEARER_READ_ONLY. Product clients' bearer APIs and the dynamic-data runtime let it read and refuse its writes as they refuse those of a read-only credential. A product's usage events are refused with impersonation_refused, whether they come with that session token or with a token granted to an impersonation. The dashboard's pages, server actions and routes do not accept it at all: the impersonation acts in the dashboard with its session cookie, as before. Reading the impersonation's login never renews it either: it ends at the time it was given, however it is presented.
Organization-scoped actors do not receive general components:manage access.
They have one bounded design exception: after an explicit prototype selection,
an actor that still has page:manage and sections:manage on its organization
may call yayaw_component_recipe_import with its active designContextId, then
call yayaw_component_publish with that same context and the exact returned
revisionId. The context brief must belong to the actor's organization, the
published registry must contain only immutable revisions from that context, and
the component must be a declarative recipe. Context-free recipe imports and
source import, registry sync, recompile, or delete remain reserved for global
component managers.
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_save saves a draft: its document is the complete
model document and becomes the latest revision, so read it with
yayaw_dynamic_data_model_get, change it, and send it back whole. Like the
other *_save tools, an optional field left out keeps its stored value: a
save without description keeps the model's description, and null (or an
empty string) clears it. Each model of yayaw_dynamic_data_bundle_apply
follows the same rule. A save, like each model of a bundle, answers a
model_setting_dropped warning for each setting of the document it could not
keep (an unknown key, an unsupported value, a list item the model refuses),
with its path, such as
ui.nav.icon or runtime.api.routes[0].preconditions[1]; the warnings come
with the answer and are not stored with the revision. The dashboard's model
builder shows the same warnings after a save.
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 reopens any model its
member may manage as a new draft, including models MCP wrote and models saved
before. The builder shows the settings it can edit and keeps every other
setting as it is: a save applies only what the person changed, from the values
the builder opened with, to the model's latest revision, and it sends
expectedRevisionId, so a model changed in the meantime answers a conflict
instead of losing that change. Lists keep their items' other keys (routes are
matched by key, row actions and event timelines by id, channels by name, rules
by position), ui.table.order, ui.table.visible and ui.form.order move
field keys in place around system columns and actions, and a menu section of
the model's own stays. When a model has settings the builder does not show,
such as a record summary, a field's maxLength, a route's value conditions or
a row action with a path of its own, the builder lists them in a notice above
its form; MCP edits them. 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. Origins listed in BETTER_AUTH_DEVICE_TRUSTED_ORIGINS receive
non-credentialed CORS on this feed, the generic runtime routes, and extension
snapshots; allowed request headers are limited to authorization, content-type,
last-event-id, and x-kyber-workspace. 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, with the action the
route declares: list, read, create, update or delete. A record write
therefore never needs manage, which also administers the model itself; a role
holding manage can still do everything. 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.
Omitted, null, or blank optional integers stay unset. In particular, omitting
expectedDataVersion does not require version 1, and omitting pagination lets
the record service apply its defaults. Explicit numeric versions still enable
optimistic locking, including values supplied as numeric strings.
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, a reference field with the auto or select widget opens a table
picker: the server reads each page, the search, and the saved value's label,
and counts every page, so any option can be found, not only the first ones.
Dynamic model references list the target model's records when the actor may
list that model: valueField provides the stored value, displayField
provides the option label, and targetScope: "same" follows the source model
scope. System references list the Yayaw system tables in the model's scope: in
an organization, its members (user), the organization itself
(organization), its teams (team), and its enabled API keys (api_key); for
a global model, every user and organization, and no team or API key.
principal lists users, then API keys, then dynamic subject models and dynamic
principal registry records, as prefixed option values. 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; each such model offers its first 50 records or search
matches, and a saved subject is found wherever it is. Other widgets, such as
radio, preload the first 100 options instead; if options cannot be loaded,
such a field 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, the first limit of
them, 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 the table, list, kanban, gallery,
calendar, chart, feed, map, file tree, and Gantt display modes (the Form
mode is added for people who can create records).
ui.table.list, ui.table.calendar, ui.table.chart, ui.table.feed,
ui.table.map, ui.table.filetree, and ui.table.gantt
hold each mode's defaults, kept exactly as Yayaw Table's own normalizers keep
them. Validation checks that they reference existing fields of a compatible
type: a calendar reads a date or timestamp start (and optional end) field, a
chart groups by scalar fields (or a JSON field shown as multi-select) and sums,
averages or bounds a number field (so does the line of a bars-and-line combo
chart, through lineMetric and lineMetricColumn), a feed reads a date
field and a text body, a map reads a location field
(map_missing_location_field when the model has none; map.locationColumn
when unset defaults to the model's first one), a file tree reads its
parent field, an optional reference of the model to itself
(filetree_missing_parent_field when it has none;
filetree.parentColumn when unset defaults to the model's only one, or
filetree_ambiguous_parent_field with several), and the Gantt reads
ui.table.gantt.startColumn and endColumn, two different date or timestamp
fields (gantt_missing_fields until both are set), with an optional
titleColumn, a parentColumn that references the model itself, and a
dependenciesColumn, a JSON field on the successor listing its predecessors
as record ids or { id, type, lag }; scheduling, parentDates,
dependencyTypes, workingDays, zoom, weekStartsOn, and
showDependencies complete it. The Gantt ignores the view's filters: it
always loads the whole model, at most 5,000 tasks, and past them its graph is
partial and read-only. A plan is written in one transaction, all or none, each
record checked against the version the Gantt loaded, for people who may
manage the model; on custom screens the Gantt only reads. Chart types are bar,
horizontal bars, line, area, combo, donut, funnel and single number, with
stacking, curve and a funnel's stageOrder. A map's titleColumn and
popupColumns read any field, colorColumn a select field whose option
colors color the markers, and style one of the dashboard's keyless
OpenFreeMap basemaps (yayaw_dynamic_data_ui_capabilities lists them under
table.modes.map); the map reads the records of the area it shows through a
bbox list scope applied as within_bounds on the location field, page by
page, and stops at 2,000 records. A file tree's nameColumn, kindColumn
(holding "folder" or "file"), sizeColumn and updatedColumn read any
field of a compatible type, and it nests records under one another along the
parent field: every parent change refused under itself or one of the records
under it (filetree_cycle), and deleting a record that still holds records
following the parent's relation.onDelete (restrict by default:
filetree_folder_not_empty). Charts and footer calculations are computed
on the server over the filtered records, calendars load only the visible
days, maps load only the shown area, a file tree reads a level at a time
(children, subtree and tree-matches list scopes, the latter two capped
at 2,000 records), and views whose advanced filters are joined with OR list
exactly the matching records.
Columns carry Yayaw Table's value formats under ui.table.columns.<field>,
with the library's own option names, so the dashboard hands them to the table
unchanged and every view shows a value the same way: table cells, list,
kanban and gallery cards, chart values, footer calculations (the sum, average,
median, bounds and range of a number column), feed properties, record details
and ui.detail summaries, exports with values as displayed, and Form view
answers. On number and integer fields, numberFormat is a separator preset
(comma, dot, space, locale) or an options object: style (decimal,
currency, percent, unit, compact), currency (an ISO 4217 code such
as EUR; alone it implies the currency style) with currencyDisplay, unit
(an Intl unit such as kilogram, or two joined with -per-) with
unitDisplay, percentBase (fraction reads 0.25 as 25%, whole reads 25 as
25%), decimals or minimumFractionDigits and maximumFractionDigits,
thousandsSeparator (none removes grouping), decimalSeparator, prefix,
suffix, and display: "bar" with max for a progress bar. On date and
timestamp fields, dateDisplayPreset picks a preset (the localized, numeric
and ISO day formats, dateTime, time, and relative), dateFormat sets a
date-fns pattern such as dd/MM/yyyy instead (it shows the viewer's local
time and English month names), and timeZone sets an IANA zone such as
Europe/Paris for the presets of a timestamp field, which the chart buckets of
that column use too. For example,
{ "numberFormat": { "style": "currency", "currency": "EUR" } } shows a price
in euros,
{ "numberFormat": { "style": "percent", "percentBase": "whole", "display": "bar" } }
shows a 0-100 progress with a bar, and
{ "dateDisplayPreset": "dateTime", "timeZone": "Europe/Paris" } shows an
instant in Paris time. Validation reports invalid_number_format_currency,
invalid_number_format_unit, invalid_number_format_fraction_digits,
invalid_number_format_progress (a progress bar needs max unless the style
is percent), invalid_date_format_pattern, conflicting_date_format (a preset
and a pattern), invalid_date_format_time_zone,
unsupported_date_format_time_zone (on a date field, or with a pattern),
number_format_field_type, and date_format_field_type as errors, and
ignored_number_format_option and date_format_time_on_date_field as
warnings. The first contract's options (separator presets, decimals,
affixes, and the day presets) on another field type stay warnings, so no
document that validated before is blocked. yayaw_dynamic_data_ui_capabilities
lists every key, allowed value, currency code, unit, example, and code under
table.valueFormats.
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, list,
calendar, chart, feed, 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, gallery
image/title/card presentation, list title and properties, calendar start, end
and title columns and layout, chart type, x axis, date period, metric and
series, and feed title, author, date, body and media columns 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, value formats, URL display
modes, JSON preview limits, tag color maps, string quote display, and
dynamic-type keys. Its number format editor offers the style (number,
currency, percentage, unit, compact) with the currency, unit, or percent base,
the decimals, affixes, separators, and a progress bar; its date format editor
offers the preset, the pattern, and the time zone of timestamp fields. Both
preview a sample value formatted by Yayaw Table, and saving applies the same
value-format checks as MCP. 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, and
optional anyOf filter groups and a dateRange (see Table views and shared
saved views below).
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. The text operators
(contains, starts_with, ends_with, not_contains) match a fragment of
the stored text: on text, email, URL, and other text-valued fields the
fragment is never validated as a whole value, so a partial email or URL typed
in a column filter narrows the list instead of failing it, and % and _ in
the fragment are matched literally. Equality and the other operators still
need a valid value of the field's type.
Location fields (type: "location") hold a place as
{ lat, lng, label?, address? } in a jsonb column, the value of YaYaw Table's
location columns. Record writes, in the dashboard and through MCP alike,
accept every form the table reads ({ lat, lng }, latitude and
longitude, lon, a GeoJSON point, a "lat, lng" text or its JSON) and
store that shape: a latitude from -90 to 90, a longitude from -180 to 180, a
name (label) of at most 200 characters and an address of at most 500. Other
values are refused (invalid_location, location_label_too_long,
location_address_too_long), never truncated. Deploying the model adds a
B-tree index on the latitude and longitude. A location column filters within
a distance (within_distance, [latitude, longitude, kilometres]), within an
area (within_bounds, [west, south, east, north], split at the
antimeridian when west is greater than east) and on being empty (is_empty,
is_not_empty: a record without coordinates is empty). PostgreSQL keeps
exactly the records the table's own location filter keeps: the distance is
its haversine with its Earth radius, narrowed by a bounding box first, and no
PostGIS is needed. A location is never sorted, grouped or summed. Create a
location field in the model builder or through yayaw_dynamic_data_model_save,
bundles and revision restores, then deploy the model to store places and
enable its map display mode. The record form accepts coordinates without a
geocoder; address search is optional.
yayaw_dynamic_data_ui_capabilities includes location in fieldTypes and
describes it under locationFields with creatable: true.
A dashboard's filters and the periods of its number widgets reach the listing
and the aggregates as required filters: they are joined with AND on top of the
view, also when the view's own filters are joined with OR, and a required
filter the table cannot apply refuses the query instead of being left out.
Date filters also take the open ranges a dashboard sends ("from" a day, "until"
a day). A YYYY-MM-DD value on a timestamp field is a calendar day: the
record's day in the viewer's time zone is compared with it, so a range of days
includes its last day. The dashboard sends the browser's zone; a query filter
takes it as dayTimeZone and uses UTC without one. Since YaYaw Table 3.8 the
table's own date filter sends the days picked (YYYY-MM-DD). Links and views
saved before it still hold each day's midnight in the viewer's time zone: on a
date or timestamp field, the dashboard reads that instant as the day it falls
on there, so a French viewer's days are not read one day early. Other values
with a time of day are still compared as instants.
A page holds at most 100 records, in the dashboard and through
yayaw_dynamic_data_records_query (limit, then offset for the next page).
The dashboard does not offer a model's larger ui.table.pageSizeOptions and
starts a larger ui.table.defaultPageSize at 100; validation warns about both
(page_size_above_limit).
A page is read where it is asked, however deep, in the dashboard's tables and
screens, the media library and the runtime API's lists: a page past the last
record holds none and the total still counts them all (the table then moves
to its last page). The MCP tool refuses an offset above 10,000. A
connection push or sync reads each record of the view once, in the view's
order, even while records are written.
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.
Table views and shared saved views
MCP prepares, humans publish. An assistant configures how a dynamic data table looks, reads its numbers, and prepares the saved views the team uses; people publish in the dashboard.
What the control plane does:
Model views.
yayaw_dynamic_data_model_saveand bundles writeui.table:displayModesamong table, list, kanban, gallery, calendar, chart, feed, map, file tree, and gantt, adefaultDisplayModefrom that list, and each mode's defaults inui.table.list,kanban,gallery,calendar,chart,feed,map,filetree, andgantt. The Form view is added automatically for people who can create records;ui.table.formView: falseremoves it.yayaw_dynamic_data_ui_capabilitiesdescribes every mode undertable.modes: its settings, the field types they need (a calendar reads date or timestamp fields, a chart's sum, average, minimum, and maximum read a number field, a feed's body is a text field, a map's location field, a file tree's parent field, a Gantt's start and end are date or timestamp fields), and its validation codes. A model offers the map mode only once it has a location field, and the file tree only once it has an optional reference to itself, its parent field. The Gantt ignores the view's filters and stops at 5,000 tasks. Every mode listed here is a model mode today (table.unsupportedModelModesis empty).Aggregates.
yayaw_dynamic_data_records_aggregateis read-only. It computes in SQL, over exactly the recordsyayaw_dynamic_data_records_queryreturns for the samefilters,anyOf,search, anddateRange, either chart groups or footer calculations. Chart groups takegroupBy(at most two levels; date and timestamp fields are bucketed by day, week, month, quarter, or year intimeZone, withweekStartsOn) andmetrics(count, sum, avg, min, max, countDistinct; at most ten). Footer calculations takecalculationsper field. It answers at most 500 groups, ordered by their keys, and setstruncated: truebeyond. It needsdynamic-data:liston the model, like the query.yayaw_dynamic_data_records_queryalso acceptsanyOf(filter groups joined with OR, as a saved view with OR filters reads its records) anddateRange(records whose days overlapfromtoto, as a calendar loads them).Shared saved views.
yayaw_table_views_list,yayaw_table_view_get, andyayaw_table_view_saveread and write the organization-shared saved views of a deployed model's records table, in any mode the table offers, Form views included (questions, sections, rules, steps layout, and fixed values). A view belongs to one organization:orgIdis the owner of an organization model, and must be named for a global model. Reading needs membership of that organization anddynamic-data:listordynamic-data:manageon the model. Saving also needscontrol-plane:write, areason, a model that allows saved and shared views, anddynamic-data:managefor a Form view. A new view belongs to the user behind the credential; only that user changes it later, with theexpectedRevisionit read. The configuration is checked like the dashboard's, and more strictly: unknown keys, unknown columns, and filter operators that do not fit their type are refused rather than dropped; list, calendar, chart, and feed settings are checked against the model's field types; values a normalizer drops come back as warnings.dryRunreturns the normalized view without writing it.yayaw_table_view_getalso returns therecordQuerythat reads the view's records through the query and aggregate tools and, for a Form view, aformPreviewof what a public link would ask, with its consents, hidden fields and languages. TherecordQueryreads the view's date rules as calendar days: a view saved before YaYaw Table 3.8 keeps the instants the browser wrote until it is saved again, and they are read as their days in UTC (the saver's time zone is not stored). A Form view without questions of its own asks the fields a person fills when creating a record, in the create form's order: never a record system column, a reference, or a hidden, edit-only, read-only, or projected field. The dashboard's Form view, the preview, and a published form ask the same ones. Form views of organization models carry theformSectionSourcea CMS form section references.
What stays in the dashboard:
publishing, updating, or unpublishing a public form link, and opening or closing it to responses. Saving a Form view never changes a published form: it keeps its snapshot until a member publishes it again;
publishing a CMS page that contains a form section (
yayaw_pages_publishrefuses it);personal (non-shared) saved views and favorites;
deleting a saved view, because it also stops the public link and the page forms built from it;
connections, destinations, pushes and syncs, sync states, schedules, and every credential or key.
yayaw_dynamic_data_ui_capabilities returns the same list under uiOnly, so an
assistant can tell a person what to finish in the dashboard.
Screens
Dashboard screens support explicitly requested MCP publication. A screen is a YaYaw Table dashboard document of sections, widgets and filters. A screen's default lives in the code; an organization's copy is stored once it customizes the screen. An assistant reads screens and prepares drafts, then publishes only after explicit user approval through the separate publication tool.
What the control plane does:
Listing.
yayaw_screens_list(orgId) lists the organization's screens the caller may view (screen:readon each: a member given access to some screens lists those alone), its system screens then its custom screens: key (the route id of the page), route, whether the organization customized it, whether the code default changed since, whether a draft is pending and who prepared it (dashboardormcp), revision, default version, andcapabilities, what the caller may do on it (view,edit,publish,manage). Itscapabilitiesdescribe what a draft may contain: YaYaw Table's grammar (the JSON Schema of a version 2 document, limited to the sources the caller may list and carrying the host blocks; versions read and written; widget, section and filter types; relative periods; limits: 256 KB, 64 levels, 40 widgets, 10 filters, 12 sections; issue codes and their severity), host blocks (where each goes, the screens that may place it, and the JSON Schema of its properties,props), and the sources the caller may list (system:<name>ordynamic-data:<scope>:<slug>).Reading.
yayaw_screen_gettakesscreenKey,orgId, optionallyinclude(default, published, draft, effective) and up to 20sourceIds. It returns the code default (document, hash, version), the published copy, the pending draft, and the effective document (what the screen shows). Each one is checked for the reader, with its issues and their JSON paths; an error would refuse a save. It also returns the descriptors of the sources those documents name and ofsourceIds: columns, and what the server answers for each (filter, sort, search, group, metric). A missing and a forbidden source both answerunavailable. The route, the caller'scapabilitieson the screen anduiOnlycome with them.Drafts.
yayaw_screen_save_draftsaves the complete document as the screen's draft (originmcp) and replaces the pending one. It needs:control-plane:write;a
reason(the draft shows its first 500 characters; the audit keeps it whole);dryRun, which is required:truechecks everything and writes nothing;expectedRevision, the revision read (0 before the screen's first save). A stale one answersscreen_conflictwith the current revision indiagnostics.currentRevision.
basedOn(effective,draftordefault) names the document the draft edits, so the screen can tell when its code default changes;defaulttakes a new default in. The tool's input describesdocumentwith the same JSON Schema. The document is checked as the dashboard's editor checks it: safe JSON, YaYaw Table's grammar (validateDashboard: repairs that keep the meaning are warnings, anything lost is an error), the screen's constraints, the sources the caller may list and the columns and display modes they have (checkDashboardReferences), and each block's properties. Errors refuse it, with their JSON paths indiagnostics.issues; warnings come back with the answer. The grammar's issues keep YaYaw Table's codes (invalidWidget,unknownColumn), the screens' own are in snake case (source_forbidden,page_table_required). The answer carries the stored document, its hash, the revision, andnextStep: explicitly requested MCP publication, or Review then Publish in the screen's status bar.Publication.
yayaw_screen_publishtakesscreenKey,orgId(orscope: "global"without an organization),expectedRevision, areasonand requireddryRun. Review the draft, preflight withdryRun: true, then usedryRun: falseandconfirm: trueat the same revision after explicit user approval. Confirmation is a client attestation, not an authorization mechanism. It requirescontrol-plane:publishand freshscreen:publishrights; global screens require globalscreen:manage. Both calls validate the stored draft and its sources as the publisher. A preflight returns the current state and candidatedocumentHashwithout changing the screen. Publication incrementsrevision, clears the draft and records its immutable version and provenance. The response includespublished,dryRun,previousRevision,revision, state and warnings. No inline document, audience, menu or record changes are accepted. First publication never grants anyone access. Missing drafts, stale revisions, archived screens and impersonation are refused. After a conflict, read and review again; after a timeout, read before retrying. Calls retain the MCP audit reason; actual publications also retain the screen history/audit.Versions. Each publication is kept as a numbered version (1, 2, …; the newest 25 of each screen, deleted with it).
yayaw_screen_revisions(screenKey,orgId) lists them, the newest first, without documents: who published each and when, the draft it published (origindashboardormcp, reason, author), the version that draft restored, the code default version it derives from, its document hash (the one the audit records) andcurrent, the version the screen shows now. Withversion, it returns that version's document and its issues for the reader (an error would refuse its restore); withcompareWithtoo (current, what the screen shows now: its published copy, else its default; or another version's number), what the version changes from that baseline, widget by widget, section by section and filter by filter: added, removed or changed, with their JSON paths and the paths of the values that differ. A version the screen no longer keeps answersrevision_not_found.Restoring.
yayaw_screen_revision_restoresaves a version as the screen's pending draft (originmcp), replacing the one waiting, and never publishes. Like a draft, it takesversion,expectedRevision(the screen's revision), areasonand the requireddryRun, and needs the same rights. The version's document is checked for the caller as any draft is (a version naming a source they may not list is refused) and derives from the code default the version derived from. The draft says which version it restores; publish through the separate MCP operation or status bar asnextStepsays, and the version that publication records names the restored one.
Every tool needs the screen-customization flag, membership of the
organization, and screen:read on the screen to list it and to read it and
its versions (a member who may view none is refused), screen:update on the
screen for drafts and restores, screen:publish for publication, or
screen:create in the organization to create a custom screen (decided
afresh), all through can().
Organization Admins hold them on every screen; others are given View, Edit or
Publish on a screen or its menu section in the dashboard
(Access to one screen): no tool changes
who has access. The tools are scoped: an organization
credential reaches its own organization only, and a credential without an
organization names one with orgId. The local stdio operator is checked like
any member. A draft is checked again for the member who publishes it. Global
screens follow their own rule, below.
Model screens
Each dynamic data model's records page is a system screen of the
organization, keyed dynamic-data.records.<model id> (a hashed segment,
h and 32 hexadecimal digits, for an id that does not fit the key rules;
see Dashboard). The screen tools take these keys like any
other: yayaw_screen_get reads one, yayaw_screen_save_draft and
yayaw_screen_revision_restore prepare its draft; yayaw_screen_publish
publishes it, as does the status bar on the model's page. A model screen exists only for a model of the
caller's organization, or a global one, that the caller may list
(dynamic-data:list or manage); otherwise its key answers
screen_not_found. Its default is one template for every model, a required
full-width table on dynamic-data:<scope>:<slug> without an inline view,
whose hash never depends on the model. yayaw_screens_list lists a model
screen once it has a row (a first draft or publication), with its page.
Custom screens
An organization's custom screens (keys custom.<random>, pages
/dashboard/screens/<slug>; see Dashboard) are listed, read
and drafted, and their versions read and restored, with the same tools and
rules as its system screens. yayaw_screens_list lists them after the system
screens, in their menu's order, with their entry under custom: slug,
English and French titles, menu placement, icon and order, page, whether
they are published or archived, and whether a person or an assistant created
them. The other tools take their key, and their route is their page. One
never published is seen by its editors only, an archived one by the
Organization Admins only: anyone else gets screen_not_found. An archived
one changes only once restored (screen_archived). They have no code
default: their default is the starter document they were created from
(version 0), never reset to.
yayaw_screen_create_draft creates one, as a draft only:
Input.
slug(lowercase words of letters and digits joined by single hyphens, at most 64 characters, unique among the organization's custom screens),title(enandfr, 1 to 80 characters each),menu(optional:placementcontent,organizationorscreens, the default, which is also its section for access;icon, one of the navigation icons;order, from -10,000 to 10,000), the complete first draft asdocument, areasonand the requireddryRun, inorgIdor the credential's organization. It takes noscope: custom screens are an organization's own.Checks. A fresh
screen:createin the organization (Organization Admins), never for an impersonation's credential; the first draft checked like any draft (YaYaw Table's grammar; the custom screens' constraints: the blocks whose registry listscustom.*, at most one full-page table on any source the caller may list, 40 widgets, 10 filters; the sources the caller may list and their columns), its errors refused with their JSON paths; the organization's limits, counted under a lock per organization: 50 custom screens, archived ones included (custom_screen_limit), and 10 created by assistants and never published (assistant_screen_limit); a slug another custom screen has answersslug_taken. A dry run checks the same, the limits and the slug as they stand, and creates nothing.What it writes. A row at revision 1 holding the first draft (origin
mcp, created by an assistant), no published copy and no access: nobody but its editors sees it before publication, and once published only the Organization Admins and whoever holds access to its menu section see it until a person gives access. Its key is drawn on creation (never on a dry run) and never changes or is reused.Answer. The screen (its key, its entry under
custom, its revision), the stored document and its hash,warnings,reviewPath(/dashboard/screens/<slug>?screen-preview=draft, where the draft is reviewed),nextStep(the separate MCP publication or status bar workflow) anduiOnly.
yayaw_screens_list also answers canCreate (whether the caller may create
one) and, for those who may, limits (each limit and how many count against
it), and describes a creation in capabilities.customScreens (constraints,
slug, titles, menu placements, icons and defaults, limits and what stays in
the dashboard). The audit records a creation with its slug as target and the
organization and slug as summary, since the key does not exist before.
Global screens
Every screen tool but yayaw_screen_create_draft also takes scope:
organization (the default) or global. A global screen, such as the admin overview
(dashboard.admin.root), has one copy for the whole platform, so a call with
scope: "global" names no orgId (invalid_input otherwise) and works on
the global screens only: yayaw_screens_list lists them, yayaw_screen_get
reads one, and yayaw_screen_save_draft saves its draft, with origin mcp,
a reason, dryRun and expectedRevision, exactly like an organization's
draft; yayaw_screen_revisions and yayaw_screen_revision_restore read its
versions and restore one as its draft. A key of the other scope answers
screen_not_found.
Who. Global screens hold the platform's layout, so listing and reading them need the same right as drafting them:
screen:managegranted without an organization, asked afresh throughcan()on every call. Only a global role binding holds it, the Super Admin's. An Organization Admin'sscreen:manageis bound to their organization and never covers a global screen (forbidden). No membership is checked.Credentials. An organization credential cannot ask for the global scope (
forbidden_resource, before any tool runs). A credential granted to an impersonation reads and never saves a draft (impersonation_refused). While thescreen-customizationflag is off, every call answerscustomization_disabled, as on an organization's screens.Sources. A global screen reads global sources only (
dynamic-data:global:<slug>, or a system source the platform declares global):capabilities.sourceslists those, and a draft naming any other is refused assource_not_global, with its JSON path indiagnostics.issues.Publishing. A Super Admin can use
yayaw_screen_publishwithscope: "global"andcontrol-plane:publish, or the screen's status bar. Organization-scoped credentials and impersonation cannot publish it.
yayaw_screens_list describes both scopes and who may use each in
capabilities.scopes. The audit records a global call with the screen key as
target and global as its summary.
What stays in the dashboard, in the status bar above the screen:
discarding a draft (Discard);
going back to the code default (Reset to default);
keeping the organization's copy after its code default changed (Keep mine).
The screen tools return this list under uiOnly, with the screen's route,
each button and the notice or badge it sits in, in English and French. A
custom screen's status bar offers publishing and discarding only (it has no
default to reset to or keep); its address, titles and menu entry, archiving,
restoring and deleting it, and giving access to it (Manage access) are
done by Organization Admins on the screen management page, on its row, as
its uiOnly says.
A draft an assistant saves also shows on the screen management pages
(Screens, in the Organization section for an organization's screens and
in the Admin section for the global ones; see Dashboard): its
row says it comes from an assistant, with its reason, author and date, and
counts among the page's drafts from assistants. A member who manages screens
reviews it there, publishes it or discards it, each change confirmed and
checked as in the status bar. These pages add no tool: yayaw_screens_list
already answers each screen's state, and yayaw_screen_publish publishes
only the exact stored draft without changing access. A screen's
versions also show in its History, in the status bar and on its row,
where a member compares them and restores one as a draft.
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:stdioMedia trash operations
yayaw_media_trash and yayaw_media_restore provide organization-scoped 30-day trash operations, for one version-checked file (assetId with expectedUpdatedAt) or a batch of up to 500 distinct files (assetIds). A batch runs in one transaction with a savepoint per file, through the dashboard's batch service, and answers each file's status in the order sent (trashed or restored, refused with a code and a reason, not_found) with counts; a refused file never stops the others. They require write access, media:manage in the organization and a reason, and default to dry-run; a batch dry run rolls its changes back and answers exactly what the call would do. yayaw_media_list accepts trashed: true. See media retention and activity for the complete contract.
Media tags
Each organization's media library has its own catalog of tags (see
Tags). Both tools name the organization (orgId,
scoped tenancy: an organization credential stays in its organization) and are
authorized by can() on media there, as the dashboard.
yayaw_media_tags_list(read,media:list) answers the tags by name, each with its color (a palette name or a CSS color) andassetCount, the files outside the trash that hold it, the limits (500 tags, 64 characters per name) anduiOnly, the steps that stay in the dashboard.yayaw_media_tags_apply(write,media:manage) adds (add), removes (remove) or sets (set, the exact tags;[]removes them all) tags on up to 500 files, in one transaction. Tags are named{ "id": … }or{ "name": … }(compared without case, accents or extra spaces);createMissing: truecreates the names ofaddorsetthat no tag has, otherwise they refuse the change (media_tags_unknown_tagwithunknownTags).addandremovekeep every other tag of each file, those added meanwhile included. It needs areasonand runs as a dry run unlessdryRun: false. It answers each changed file's tags before and after (ids and names), the files already as asked (unchanged), the files in the trash or outside the library (failed), the tags created and the change'stransactionId. Each changed file gets an activity entry its author can undo in the dashboard; the file itself (itsupdatedAt) does not change.yayaw_media_listandyayaw_media_searchreturn each file'stags(ids, names and colors, in the order they were given) and filter ontagIds(files holding every one of them).
Renaming, recoloring, merging and deleting tags stay in the dashboard (the media library's Tags column › Manage tags): they change every file at once and cannot be undone. Undoing a change of tags stays there too.