The data-product-ui/v1 contract lets a publisher deploy one interface and use it
in independently built catalogues. It defines public metadata, a restricted
iframe, and two optional presentation capabilities. The reference registry and
the standalone compatibility kit both implement it. Neither imports product code
into its own document, proxies product requests, or owns the product runtime.
The OpenFeature flag ui-contract defaults off in the controller, demo product,
and kit. Set UI_CONTRACT_ENABLED=true explicitly for a development evaluation.
The chart exposes uiContract.enabled: true. The registry workspace is available
by default. With routing enabled, the chart declares the catalogue
origin in the demo manifest and in the demo's separate UI_HOST_ORIGINS setting.
Use uiContract.additionalHostOrigins for independently deployed compatibility
hosts. The chart includes the registry origin automatically and uses the same
validated list in both declarations:
uiContract:
enabled: true
additionalHostOrigins:
- https://kit.exampleUp to fifteen additional origins are supported. Each must be a distinct canonical HTTPS origin; repeating the registry origin is rejected. The existing flag still defaults off, and a disabled UI contract publishes no host grants.
A contract-bearing UI is unavailable in the registry while the flag is off.
It does not fall back to a legacy frame. Existing descriptors without contract
continue to use message-free sandboxed embedding. Rollback disables the flag and
reloads host pages; already loaded browser sessions are not remotely revoked.
Apply the updated CRD before upgrading an existing Helm installation, as described
in the installation guide.
Platform owns production activation and immutable artifact pins. Release, local browser tests, and a successful handshake do not prove a production rollout. Issue #120 tracks rollout acceptance and eventual removal of the temporary flag.
The standalone manifest is exactly the descriptor's ui object. Publish this
metadata in spec.ui or distribute the JSON independently. The hosts never fetch
an arbitrary manifest URL. See the example:
{
"url": "https://products.example/harbour/ui",
"title": "Explore harbour observations",
"contract": {
"apiVersion": "data-product-ui/v1",
"hostOrigins": ["https://catalog.example", "https://kit.example"],
"capabilities": ["status", "resize"]
}
}All fields shown are required and unknown fields are rejected by the browser
validator. The compatibility kit identifies missing fields before opening a
product frame. Copy the public descriptor's ui object, including its title.
Legacy descriptors may omit contract; obtain the publisher's supported contract
before connecting through the kit. Adding metadata alone does not make a legacy
UI implement the protocol or approve a host. The kit requires apiVersion,
hostOrigins and capabilities in the contract, even when no optional hints are
requested. Use capabilities: [] in that case. A manifest
is at most 16 KiB of UTF-8 JSON; a title has 1–200 characters. The HTTPS entrypoint
is at most 2,048 characters, with no credentials, whitespace, backslashes, or
fragment. URLs and manifests are public metadata: never put credentials in a URL,
query string, title, or other field.
Declare 1–16 distinct exact HTTPS host origins, each at most 253 characters.
Use lowercase DNS names or canonical IPv4 addresses; use a non-default port only
when needed (https://127.0.0.1:8443). Paths, trailing slashes, wildcard hosts,
credentials, queries, fragments, IPv6 literals, and explicit default :443 ports
are not part of the v1 origin subset. Browser validation is stricter than the
CRD's structural schema and refuses noncanonical declarations before navigation.
The manifest allowlist is one side of the agreement. Configure the product's
own deployment with its approved hosts too. The demo reads UI_HOST_ORIGINS, a
comma-separated list with no spaces, only when its release flag is on. Its public
/ui-contract-config response contains that list and the boolean appearance gate. Do not accept an allowlist
supplied in the host's initialization message. No user identity, Kubernetes
identity, access token, cookie, or dataset passes over this protocol.
Every message is an object with exactly the documented keys. All messages carry
apiVersion: "data-product-ui/v1" and the current session string. Unsupported
versions, unknown fields, stale sessions, and ungranted hints are ignored.
| Direction | Type | Additional fields | Meaning |
|---|---|---|---|
| Host → product | init |
capabilities: [] |
Fresh UUID session and intersection of requested capabilities with host policy |
| Product → host | ready |
None | This document accepted initialization and speaks v1 |
| Product → host | status |
state: "ready" or "error" |
Optional interface status hint; requires status grant |
| Product → host | resize |
height: integer |
Optional height in CSS pixels, 240–1200 inclusive; requires resize grant |
The registry grants status and resize when requested; these are its fixed
local presentation policy. The kit grants neither until its operator selects
the respective checkboxes. Hosts must never interpret a message as controller
readiness, data freshness, permission to query, or a change to access policy.
The ready handshake is a protocol signal and requires no optional capability.
The host uses sandbox="allow-forms allow-scripts", without
allow-same-origin, and referrerpolicy="no-referrer". Product messages therefore
have the opaque origin "null". The host requires all of: that origin, the exact
iframe contentWindow, the current unpredictable session, the exact message
shape, and the relevant capability. A null origin alone authenticates nothing.
The host sends initialization only to that iframe window. Its target origin must
be "*" for opaque-origin delivery; this envelope contains public protocol
metadata only. See the browser's
postMessage rules.
The product accepts initialization only from window.parent, at an exact origin
in its own allowlist, with the documented version, shape and capabilities.
It replies with the exact event.origin as targetOrigin. See the complete
demo implementation. Public query endpoints
used by an opaque frame must support credential-free CORS. The demo uses a public
API; authenticated products need their own separately designed access mechanism.
V1 supplies no authentication or credential-transfer capability.
Every frame load rotates the session. Selection changes, closing, and a ten-second
handshake timeout revoke it and remove the frame. More than 256 matching-session
messages also closes the frame with an error; hints are for discrete UI changes,
not continuous telemetry. Select the product again to start a fresh session. A
query failure can report error followed by ready on recovery within a session.
In the kit, a host-closed interface directs the user to Validate and open; a product-reported error keeps its own recovery controls available. The kit and demo servers bound request reads to ten seconds, writes to fifteen seconds, and idle connections to sixty seconds.
The sandbox does not certify publisher code, prevent every network request it can make, or authenticate a redirected destination. Loaded code remains untrusted. Hosts need their own publication policy, TLS, CSP, and product access controls. The kit checks interoperability, not publisher security or universal accessibility.
data-product-ui/v1 remains limited to status and resize. The v2 manifest uses
the same closed fields and HTTPS origin constraints, with at most three distinct
capabilities: status, resize, and optionally appearance. Admission rejects
an appearance capability under v1. A v2 host additionally requires the default-off
OpenFeature ui-appearance gate; it refuses a v2 frame before navigation when
disabled. Set UI_APPEARANCE_ENABLED=true alongside UI_CONTRACT_ENABLED=true,
or both chart values uiAppearance.enabled and uiContract.enabled, explicitly.
The reference sample requires the same two gates and its own approved host origins.
When the chart's sample is enabled, v2 also requires a configured chart route to
declare the publisher-approved catalogue origin. A registry-only installation may
enable v2 for independently configured external products without a chart route.
Init, ready, status and resize keep their existing exact shapes, carrying the negotiated v1 or v2 version. After the current v2 ready handshake, the host may send:
{
"apiVersion": "data-product-ui/v2",
"type": "appearance",
"session": "<current UUID>",
"appearance": "light"
}The only values are light and dark. The workspace resolves System locally,
using its media query, and sends a changed enum when the system preference changes.
It sends nothing without both a publisher request and the host's explicit appearance
grant. A product accepts the hint only from its approved parent origin and exact
window, under the current v2 session and negotiated grant. Unknown keys, stale
sessions, other versions and arbitrary CSS or URLs are ignored. Both sides bound
appearance updates to 256 per session; the host suppresses duplicate values.
Navigation revokes the previous session and reapplies the current theme only
after the next ready handshake. No frame reload, query, credential, storage command
or sandbox permission is part of an appearance update.
The registry's selector offers System, Light and Dark, retaining an explicit choice where browser storage is available. Storage denial does not block queries. Legacy UIs and products without an appearance grant retain their own theme. The gate is a long-lived operator choice controlling optional presentation grants. Platform owns its production activation and readback in issue 146. ADR 0011 records the rationale.
Another host opts in with appearanceEnabled: true, an appearance grant and
appearance: "light" or "dark" when calling mount. Its returned disposer has
setAppearance(value) for later changes; dispose before another mount. The kit
offers System, Light and Dark for its own workspace in either gate state. It reveals
the v2 checkbox only with the additional gate and sends appearance hints only when
the operator checks that box. The optional preference survives reload when browser
storage is available; denied storage does not prevent live changes. A static kit host
opts in by setting data-appearance-enabled="true" on its body; this is an
explicit deployment choice, just as serving the static kit itself is.
For the signed image command, health probes and HTTPS gateway deployment, see the portable host deployment guide.
Use a TLS certificate trusted by your browser for the chosen host. The Go command serves only static assets and has no Kubernetes or registry dependency:
UI_CONTRACT_ENABLED=true go run ./cmd/ui-kit \
--listen-address 127.0.0.1:8443 \
--tls-cert /path/to/local-cert.pem \
--tls-key /path/to/local-key.pemOpen https://127.0.0.1:8443, include that exact origin in the manifest and the
product's own allowlist, paste the manifest, choose grants, and select Validate
and open. Keep private keys out of the repository. The demo command is HTTP
behind an independently managed TLS endpoint; the kit does not terminate TLS for
the product. When testing the chart's demo in a second host, explicitly configure
that additional origin through uiContract.additionalHostOrigins. For products
published outside the chart, configure both their manifest and their own deployment.
Editing a manifest or descriptor, replacing its selected file, or changing a grant closes the active interface and cancels pending imports. Choose Validate and open or Import and open again to use the revised declaration and permissions. Leaving the page closes its session; restoring browser history does not reopen it automatically.
Local JSON imports reject duplicate decoded fields, including escaped aliases, before navigation. Numeric metadata must be mathematically integral and within JavaScript's safe integer range before rounding; integral decimal and exponent notation remain supported. Clear file withdraws the selection and any unfinished file read. Selecting a file clears pasted JSON; typing JSON clears the selected file. Protocol closure offers Retry descriptor or Retry manifest for the last input format; retry validates the current input and grants and establishes a new session only after that explicit action.
Alternatively, serve web/index.html, kit.css, kit.js, descriptor.js, and ui-contract.js
from any HTTPS static host. Apply the policy in web.KitHandler: same-origin
scripts/styles, no host connections, HTTPS frames, no ancestors, objects or base
URI, no referrers, and no camera, microphone or geolocation permissions. That
static deployment is an explicit opt-in; it has no Go/OpenFeature process gate.
To build another host, load web/ui-contract.js in your own document and call:
const dispose = DataProductUI.mount({
frame: document.querySelector("iframe"),
manifest,
grants: ["status"],
onState: (state) => {
/* Render your own accessible loading/ready/error/timeout text. */
},
});
// Before selecting another product or closing this one:
dispose();DataProductUI.validate(manifest, location.origin) validates without navigating.
DataProductUI.validateMetadata(manifest) validates the public presentation shape
without granting host permission. Descriptor inspection uses that limited operation;
both validate and mount still require exact publisher approval for the actual host.
DataProductUI.validateURL(value) checks the literal public HTTPS metadata profile
without fetching it. UI declarations, descriptor links and trace references use
the same profile. Canonical local HTTPS and IP destinations are supported; URL
normalization cannot repair an invalid declaration into an accepted destination.
mount validates again, throws on refusal, and returns an idempotent disposer.
Catch refusal and render it as inert text. Do not run multiple mounts on the same
frame; dispose the current one first. The library is framework-independent.
Run go test -tags=browser ./internal/browser to exercise actual Chromium against
independent TLS fixture servers. The suite verifies both release states, the same
demo in both hosts, queries, error recovery, denied origins, bounded capabilities,
wrong-window/origin/session rejection, navigation revocation, message limits, and
handshake timeout cleanup. Browser certificate bypass is confined to these local
test fixtures; production hosts must use verified HTTPS.
For a third-party UI, use the kit and check all of the following:
- A manifest for the wrong host or version is rejected without opening a frame.
- The loading message changes only after a compatible handshake; blocked or unavailable pages time out and can be retried.
- Query controls work with the keyboard, have labels and visible focus, and status changes are announced. Closing the interface is keyboard accessible.
- At 375 pixels wide, controls and results fit without horizontal scrolling.
- A query failure reports a useful message; a successful retry restores results.
- Unchecking a grant prevents that hint from affecting the host. Closing or navigating the frame makes the old session unusable.
The bundled browser tests cover keyboard submission, narrow-screen overflow and live status semantics, but do not replace screen-reader testing of each product. Products that cannot use the protocol can keep their existing message-free UI declaration. ADR 0007 records the boundary.