Server-Driven UI in 2025: Versioned Layout Schemas, Capability Negotiation, and Safe Mobile Rollouts
Cut app store bottlenecks with SDUI done right: JSON/Wasm layouts, backward-compatible schemas, client capability flags, A/B experiments, offline fallbacks, security hardening, and tooling to debug and verify changes before they ship.
Server-Driven UI (SDUI) isn’t new, but the way we should do it in 2025 is materially different from the ad-hoc template pushing of the last decade. We have better primitives (Wasm, robust schema tooling, mature feature flags), faster JSON parsing, richer device capabilities, and a much clearer understanding of security, privacy, and observability concerns.
This article proposes a pragmatic, production-grade blueprint for SDUI that:
- Defines versioned layout schemas (with additive evolution, deprecation windows, and strong validation)
- Negotiates client capabilities up front (so the server only ships what the client can render)
- Supports JSON layouts for portability and Wasm extensions for bounded dynamic logic
- Enables safe mobile rollouts with flags, experiments, and kill-switches
- Works offline gracefully, with deterministic fallbacks
- Hardens the surface against malicious or corrupt payloads
- Comes with tools to debug, verify, and continuously ship changes without the app store bottleneck
If you’ve tried SDUI and burned your fingers on brittle contracts, unsafe rendering, or un-debuggable experiments, this guide is intentionally opinionated to help you avoid those pitfalls.
TL;DR
- Use explicit, versioned UI schemas and generate native types from them; additive changes only; validate everything at the edge.
- Add a first-class capability negotiation handshake (transport + render + behavior), and only serve layouts the client proves it can handle.
- Build layouts as JSON ASTs; extend with optional Wasm components for small, sandboxed functions (e.g., local formatting, pure transforms), not long-running business logic.
- Ship UI changes with feature flags, A/B experiments, and server-side kill switches; do canary + dark mode + staged rollouts.
- Always include offline fallbacks and pre-bundled base components; cache, prefetch, and pre-validate.
- Sign, validate, and log everything; treat layouts as untrusted input.
- Invest in tooling: schema diffing, layout previewers, screenshot testing, property-based validation, and traceable rollouts.
Contents
- Why SDUI in 2025 (and why teams get it wrong)
- A reference architecture for SDUI
- Versioned layout schemas (JSON Schema, Protobuf, compatibility)
- JSON layouts and the case for optional Wasm
- Capability negotiation: flags, versions, and fallbacks
- Safe rollouts: flags, experiments, and kill switches
- Offline and degraded modes that don’t break
- Security hardening for SDUI
- Tooling and verification in the delivery pipeline
- Performance: caching, patching, and minimizing overfetch
- Observability: logs, metrics, traces you actually need
- Migration plan and anti-patterns
- Checklist and conclusion
Why SDUI in 2025 (and why teams get it wrong)
The pitch: you push UI changes from the server without waiting for app store approvals. That part is true. But teams frequently fail due to:
- Weak contracts: unversioned, implicit schemas that break older clients
- Launch risk: pushing layouts that clients can’t render, or slow down
- Security gaps: treating configuration as trusted code
- Observability gaps: no way to correlate a layout change to a crash or conversion drop
- Poor tooling: no way to preview or validate layouts against real devices
The fix is not “more flexibility,” but more discipline: explicit schemas, capability negotiation, safe rollout practices, and tight feedback loops.
A reference architecture for SDUI
-
Client
- Renderer for a finite set of native components (Button, Text, Image, List, WebView, Map, etc.)
- JSON schema validator and decoder; optional Wasm runtime for micro-logic (WASI or component model)
- Capability reporter (versions, feature flags, performance hints)
- Caching layer: content-addressed layout cache + image/assets cache
- Analytics hooks: view exposure, click events, layout decoding errors
-
Server
- Layout service: produces versioned layout JSON (and optional Wasm blobs) based on feature flags, A/B buckets, and client capabilities
- Experiment/flag service: cohorts, rollout policies, kill switches
- Schema registry: validates outgoing layouts; enforces compatibility rules
- CDN for layouts and assets with signed URLs and ETags
- Observability pipeline: logs layout IDs, client capabilities, decode/render metrics, and user outcomes
-
Delivery
- CI/CD that runs schema diff checks, property-based layout validation, snapshot/screenshot tests, and deploys to canary cohorts first
Versioned layout schemas
Schema discipline is the difference between repeatable success and a continuous stream of hotfixes. Pick a format that supports code generation and validation. Many teams use:
- JSON Schema for layouts (with codegen to TypeScript/Kotlin/Swift)
- Protobuf for compact transport; JSON for debuggability; sometimes both
- Avro/FlatBuffers/Cap’n Proto if you need zero-copy or more extreme performance
The core rules:
- Prefer additive changes (add fields, do not repurpose meanings)
- Use explicit versioning at the schema and message level (major.minor)
- Keep changes backward-compatible for at least one client LTS window
- Deprecate with telemetry: measure when it’s safe to remove support
- Validate layouts at the edge and again on the client (fast-fail)
Example: JSON Schema for a Card-based feed
json{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/schemas/layout/v1", "title": "LayoutV1", "type": "object", "properties": { "schemaVersion": { "type": "string", "pattern": "^1\\.[0-9]+$" }, "layoutId": { "type": "string" }, "components": { "type": "array", "items": { "$ref": "#/definitions/Component" } } }, "required": ["schemaVersion", "layoutId", "components"], "additionalProperties": false, "definitions": { "Component": { "type": "object", "oneOf": [ { "$ref": "#/definitions/Text" }, { "$ref": "#/definitions/Image" }, { "$ref": "#/definitions/Button" }, { "$ref": "#/definitions/List" } ] }, "Text": { "type": "object", "properties": { "type": { "const": "text" }, "id": { "type": "string" }, "value": { "type": "string" }, "style": { "$ref": "#/definitions/TextStyle" } }, "required": ["type", "id", "value"] }, "Image": { "type": "object", "properties": { "type": { "const": "image" }, "id": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "alt": { "type": "string" }, "aspectRatio": { "type": "number", "minimum": 0.1, "maximum": 10 } }, "required": ["type", "id", "url"] }, "Button": { "type": "object", "properties": { "type": { "const": "button" }, "id": { "type": "string" }, "label": { "type": "string" }, "action": { "$ref": "#/definitions/Action" } }, "required": ["type", "id", "label", "action"] }, "List": { "type": "object", "properties": { "type": { "const": "list" }, "id": { "type": "string" }, "items": { "type": "array", "items": { "$ref": "#/definitions/Component" } } }, "required": ["type", "id", "items"] }, "Action": { "type": "object", "properties": { "type": { "enum": ["deeplink", "http", "event"] }, "value": { "type": "string" }, "payload": { "type": "object", "additionalProperties": true } }, "required": ["type", "value"] }, "TextStyle": { "type": "object", "properties": { "weight": { "enum": ["regular", "medium", "bold"] }, "size": { "type": "number", "minimum": 8, "maximum": 64 }, "color": { "type": "string", "pattern": "^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" } }, "additionalProperties": false } } }
You can evolve this by adding new components under definitions and allowing the server to conditionally include them only when clients advertise support.
Server-side validation gate
- Lint: enforce field naming, consistent units, and style tokens
- Validate: run JSON Schema validation on every layout in staging and production
- Sanitize: strip unknown fields for strict clients
- Log and tag: attach layoutId and schemaVersion as trace attributes
Protobuf for compactness (optional)
For latency-sensitive paths, keep a parallel Protobuf definition and publish both from the server. Clients can opt into Protobuf based on negotiation.
protosyntax = "proto3"; package layout.v1; message Layout { string schema_version = 1; // e.g., "1.7" string layout_id = 2; repeated Component components = 3; } message Component { oneof kind { Text text = 1; Image image = 2; Button button = 3; List list = 4; } } message Text { string id = 1; string value = 2; TextStyle style = 3; } message Image { string id = 1; string url = 2; string alt = 3; float aspect_ratio = 4; } message Button { string id = 1; string label = 2; Action action = 3; } message List { string id = 1; repeated Component items = 2; } message Action { enum Type { DEEPLINK = 0; HTTP = 1; EVENT = 2; } Type type = 1; string value = 2; map<string, string> payload = 3; } message TextStyle { enum Weight { REGULAR = 0; MEDIUM = 1; BOLD = 2; } Weight weight = 1; uint32 size = 2; string color = 3; }
Generate code for Swift/Kotlin/TypeScript and keep the generated types checked into the app repo so rendering stays type-safe and fast.
JSON layouts and the case for optional Wasm
JSON layouts are portable, human-readable, and trivial to log and diff. But sometimes you need small pieces of dynamic behavior: format a price per locale, run a pure transform, or compute a conditional visibility rule. That’s where Wasm can help—if used judiciously.
- JSON drives structure; Wasm provides bounded pure functions
- Keep Wasm functions deterministic and side-effect free; no network or disk
- Expose a tiny ABI: e.g., parse/format, pure mapping, simple math; time and randomness should be injected to remain testable
- Cache Wasm modules by content hash; sign them; validate size and imports
Example: layout with a Wasm formatter
json{ "schemaVersion": "1.7", "layoutId": "home-2025-09-12", "components": [ { "type": "text", "id": "greeting", "value": "Welcome" }, { "type": "text", "id": "price", "value": "", "style": { "weight": "bold" } }, { "type": "button", "id": "cta", "label": "Buy now", "action": { "type": "deeplink", "value": "app://checkout" } } ], "extensions": { "wasm": [ { "id": "fmt_price@1", "url": "https://cdn.example.com/wasm/fmt_price_v1.wasm", "sha256": "8ae...", "exports": ["format_price"] } ], "bindings": [ { "componentId": "price", "export": "format_price", "args": { "amount": 1299, "currency": "USD", "locale": "device" } } ] } }
On the client, the renderer loads the Wasm module, calls format_price(amount, currency, locale)
with a tiny JSON-to-ABI marshaller, and injects the returned string into the Text component. If the module is absent or fails validation, the renderer falls back to a safe, generic formatter or a server-supplied string.
A minimal Wasm function (Rust)
rust// Cargo.toml // [lib] // crate-type = ["cdylib"] #[no_mangle] pub extern "C" fn format_price_c(amount_cents: i64, currency_ptr: *const u8, currency_len: usize, locale_ptr: *const u8, locale_len: usize) -> i64 { // For brevity: in real code, pass/return via linear memory or canonical ABI and avoid heap leaks. // Compute a scaled integer and return a handle, or use wit-bindgen in 2025 for canonical ABI. // Here, just return amount_cents to keep it trivial. amount_cents }
In 2025, prefer the WebAssembly Component Model and language toolchains that generate stable bindings for your host environment. Keep the ABI small and explicit.
Capability negotiation: flags, versions, and fallbacks
Never assume a client can render the latest layouts. Negotiate.
- Client announces: app version, schema support ranges, component support, media codecs, transport formats, max payload, Wasm support, CPU/GPU class
- Server responds with a compatible layout or an older variant plus an upgrade hint
Capability envelope example
json{ "app": { "name": "ExampleApp", "version": "12.4.3", "platform": "ios", "osVersion": "18.1" }, "schema": { "layout": { "min": "1.0", "max": "1.7" } }, "components": ["text", "image", "button", "list"], "formats": { "accept": ["application/sdjson+v1", "application/proto+v1"], "encoding": ["gzip", "br"], "wasm": true }, "limits": { "maxLayoutKB": 256, "maxWasmKB": 64 } }
Send that with every layout request (or cache on session). The server picks the best format and set of features.
HTTP-level hints
- Accept header:
Accept: application/sdjson+v1; q=1.0, application/proto+v1; q=0.8
- Client-Features:
Client-Features: sd_schema=1.7,comp=list|carousel,wasm=1,offline=1
- Prefer:
Prefer: minimal
for low bandwidth or battery saver modes
Server selection algorithm (pseudocode)
if client.schema.max < server.required_min_schema:
return 426 Upgrade Required with upgrade_url
features = intersect(client.components, server.components)
format = best_match(client.formats.accept, ["application/proto+v1", "application/sdjson+v1"])
wasm_ok = client.formats.wasm && client.limits.maxWasmKB >= module.size
layout = assemble_layout(schema=client.schema.max, features=features, format=format, wasm=wasm_ok)
return 200 layout
Log the decision. If you ship a downgraded variant, include a subtle prompt or telemetry to prioritize client upgrades.
Safe rollouts: flags, experiments, and kill switches
Treat UI changes as experiments by default.
- Flags express exposure conditions: app version ranges, capability predicates, cohort membership, geo/time, device class
- Experiments define buckets, metrics, and stop conditions
- Kill switches disable a variant server-side without new client builds
Example: bucketing and flag evaluation
tstype Capabilities = { schemaMax: string; wasm: boolean; components: string[] }; type Rule = (ctx: { userId: string; appVersion: string; caps: Capabilities }) => boolean; const supportsCarousel: Rule = ({ caps }) => caps.components.includes("carousel"); const isModernApp: Rule = ({ appVersion }) => semverGte(appVersion, "12.0.0"); function bucket(userId: string, key: string, buckets: number): number { const h = murmur3(`${key}:${userId}`); return h % buckets; } const experiment = { key: "home_carousel_v2", enabledIf: (ctx) => isModernApp(ctx) && supportsCarousel(ctx), buckets: 100, variants: [ { name: "control", range: [0, 49] }, { name: "treatment", range: [50, 99] } ] }; function assign(ctx) { if (!experiment.enabledIf(ctx)) return "control"; const b = bucket(ctx.userId, experiment.key, experiment.buckets); return b <= experiment.variants[0].range[1] ? "control" : "treatment"; }
The server assembles the layout for the assigned variant and capabilities. Metrics (exposures, CTR, conversion, decode time, render stability) feed back into an automated guardrail: auto-disable if crash or latency thresholds are crossed.
Staged rollout policy
- Stage 0: internal users and canary devices
- Stage 1: 1% of eligible population, monitor errors P95/P99
- Stage 2: 10% if guardrails hold
- Stage 3: 50%+ or full rollout with backstop kill switch
If you can only do one thing: make kill switches universal and fast. A feature behind a flag shouldn’t be one network hop away from being disabled.
Offline and degraded modes that don’t break
SDUI can fail inelegantly without offline design. Your client app must have a base UI kit that renders a minimal experience without server data. Then layer SDUI layouts when available.
- Pre-bundle essential components: header, navigation, skeleton states, critical flows
- Cache recent layouts by content hash; serve cached layout if network fails
- Provide default data for key text strings and images; use placeholders that match the design system
- Validate cached layouts against current capabilities; if incompatible, gracefully downgrade
Offline flow
- Attempt to fetch new layout (If-None-Match/ETag)
- If 304, reuse cached layout
- If network fails, load last known-good layout for the route
- If none exists or incompatible, render built-in fallback screen with a simple CTA
Example: layout caching key
json{ "cacheKey": "route:home:capHash:abcd1234", "etag": "W/\"layout-home-2025-09-12:sha256-XYZ\"", "ttl": 86400, "fallback": "builtin:home_v1" }
Keep fallbacks boring and predictable. Never let a missing layout block critical actions (e.g., checkout, authentication).
Security hardening for SDUI
Treat layouts as untrusted input. Your biggest risks:
- Data exfiltration via injected URLs or event payloads
- Arbitrary code execution via interpreters (including Wasm if you expose rich host APIs)
- Phishing-like UI that mimics sensitive flows
- Denial of service via extremely deep or large layouts
Mitigations:
- Sign layouts and Wasm modules; verify Ed25519 signatures against a pinned public key bundle shipped with the app; include expiring timestamps to limit replay
- Validate against schema at the edge and again on-device; enforce hard caps on depth, breadth, and total size
- Sanitize URLs: allowlist schemes and hosts; strip query parameters for sensitive actions; enforce HTTPS
- Lock down host functions available to Wasm: no network, no file I/O, no clipboard, no sensors; time and locale exposed as pure inputs only
- Enforce Content Security Policies for any embedded WebView components; disable inline scripts; use origin isolation
- Apply UI constraints: certain component types (e.g., PIN entry) must be local/native-only and never server-driven
- Rate limit layout requests to throttle abuse; bind to session/device identity and rotate tokens
Log every violation with layoutId, signature status, and reason; route to your security pipeline.
Tooling and verification in the delivery pipeline
You cannot do SDUI safely without good tooling.
- Schema registry and diffing: generate human-readable change logs; block breaking changes by default
- Layout previewer: load a layout JSON/Wasm locally into a renderer for iOS/Android/Web; simulate device classes, locales, accessibility settings
- Screenshot testing: golden images per device class; detect pixel drift
- Property-based tests: generate random but valid layouts to fuzz your renderer
- Static analyzers: ensure accessibility (contrast, sizes), content rules, and analytics coverage
- Release gates: require canary success and guardrail metrics before ramping
Example: schema diff check output
ChangeSet: layout.v1 -> layout.v1.8
- Add component type: carousel (additive)
- Add Button.action.payload field: optional (additive)
- Deprecate Text.style.size: use style.token instead (compatible for 2 minor versions)
Verdict: compatible; warn on deprecated usage.
Local preview CLI sketch
$ sdui preview --layout home.json --device iphone15 --locale de-DE --a11y large-text --caps caps.json
✓ Schema validated (v1.7)
✓ Wasm module fmt_price@1 verified (sha256 matches, imports ok)
✓ Render time P50: 9.2ms, P95: 15.3ms
✓ All actions mapped to handlers
Rendered screenshot -> ./out/home.iphone15.de-DE.png
Publish the previewer as a devtool and a CI job. Designers and PMs should be able to use it.
Performance: caching, patching, and minimizing overfetch
SDUI can be fast if you treat layouts like content:
- Cache aggressively with ETags and long-lived immutable content hashes (e.g., layoutId includes sha256)
- Use delta updates: JSON Patch or custom diffing for large screens that change slightly
- Compress with Brotli; prefer Protobuf for large lists on slow connections
- Split heavy assets; lazy-load below-the-fold sections via subsequent requests
- Minimize nesting depth; flatten where possible
- Pre-resolve style tokens on the server; send numeric values to reduce decode time
- Measure decode and render times on-device and feed back into the layout service to budget complexity
Example: JSON Patch for a feed update
json[ { "op": "replace", "path": "/components/3/label", "value": "Shop now" }, { "op": "add", "path": "/components/5", "value": { "type": "image", "id": "promo", "url": "https://cdn.example.com/promo.png" } } ]
Only ship the patch if the client’s base ETag matches; otherwise, fall back to full layout.
Observability: logs, metrics, traces you actually need
Without observability, SDUI is guesswork. Capture:
- Layout exposure events: layoutId, schemaVersion, variant, capability hash, ETag, source (cache/CDN/origin)
- Decode metrics: size, parse duration, Wasm validation and execution time, component counts and depth
- Render metrics: frame time, ANR/hitch counts, memory deltas
- Errors: schema violations, unknown components, missing assets, action failures
- Business outcomes: CTR, conversion, retention, task completion
- Rollout attribution: flag keys, variant names, cohort IDs
Use distributed tracing from the layout service through CDN to client events; add layoutId to every span. Build a “layout timeline” view so you can correlate an experiment flip with UI stability and KPIs.
Migration plan and anti-patterns
A pragmatic migration plan
- Inventory components: define the initial server-driven surface (e.g., home feed and promotions, not auth or payments)
- Author the first schema: keep it small and explicit; generate client types
- Implement a thin renderer: only a handful of components; strict validation
- Build the layout service with capability negotiation and a single flag
- Integrate observability and a minimal preview tool
- Ship to internal/canary users and iterate
- Gradually expand component coverage and experiment surface
Anti-patterns to avoid
- Unbounded scripting: shipping arbitrary JavaScript or Lua interpreters with full platform access; this is a security and performance time bomb
- Implicit contracts: relying on “conventions” rather than schema and validation
- Everything SDUI: resist the urge to server-drive critical security flows, core navigation, or platform modalities that benefit from native affordances
- No fallback strategy: assuming the network will always be there
- Monolithic layouts: rendering a single megabyte JSON blob for a complex app screen
- One-way experiments: forgetting to tie UI variants to analytics and guardrails
Concrete examples and industry notes
- Shopify’s Remote UI demonstrates a component-protocol approach to UI that’s conceptually adjacent to SDUI: a remote component tree rendered natively with strict contracts and tooling. See Shopify Engineering on Remote UI.
- Content-heavy apps (e.g., news, commerce, streaming) often use SDUI for home feeds and promotions while leaving core secure flows native.
- Connected TV apps have long embraced server-driven experiences due to long release cycles and device fragmentation; the lessons around capability negotiation translate well to mobile.
The pattern is consistent: viable SDUI systems invest heavily in schemas, tooling, and guardrails. The rendering engine itself is comparatively simple.
Implementation snippets: client-side renderer sketch
Here’s a tiny TypeScript-like pseudocode renderer that shows the flow—validate, hydrate, bind actions, and render:
tstype Layout = {/* generated from schema */}; type RenderContext = { capabilities: object; actions: Record<string, (payload: any) => void>; wasm: WasmHost; }; async function renderLayout(json: unknown, ctx: RenderContext): Promise<HTMLElement> { const layout = validateLayout(json); // throws on invalid // Resolve extensions if (layout.extensions?.wasm) { for (const mod of layout.extensions.wasm) { await ctx.wasm.loadModule(mod.url, mod.sha256); } } // Hydrate components for (const b of layout.extensions?.bindings ?? []) { const fn = ctx.wasm.getExport(b.export); const out = await fn(b.args); const comp = layout.components.find(c => c.id === b.componentId); if (comp?.type === 'text') comp.value = String(out); } // Render const root = document.createElement('div'); for (const c of layout.components) root.appendChild(renderComponent(c, ctx)); return root; } function renderComponent(c: any, ctx: RenderContext): HTMLElement { switch (c.type) { case 'text': { const el = document.createElement('span'); el.textContent = c.value; applyTextStyle(el, c.style); return el; } case 'image': { const el = document.createElement('img'); el.src = sanitizeUrl(c.url); el.alt = c.alt || ''; return el; } case 'button': { const el = document.createElement('button'); el.textContent = c.label; el.onclick = () => handleAction(c.action, ctx); return el; } case 'list': { const el = document.createElement('div'); for (const item of c.items) el.appendChild(renderComponent(item, ctx)); return el; } default: return fallbackUnknownComponent(c); } }
Even in a web context, the same design applies: validate strictly, sanitize URLs, and gate features behind capability flags.
Practical checklist for SDUI done right
- Schemas
- Versioned, additive-first; deprecation windows and telemetry
- Codegen to native types; strict validation on server and client
- Capability negotiation
- Client advertises schema range, component list, formats, size limits
- Server downshifts features; logs decision
- Layouts and behavior
- JSON layouts as the baseline; optional Wasm for pure, small functions
- Content-addressed caching; ETags and immutable URLs
- Rollouts
- Feature flags, staged experiments, kill switches
- Guardrails: crash/latency thresholds auto-disable
- Offline
- Pre-bundled fallbacks; cache last-known-good
- Deterministic downgrades when capabilities change
- Security
- Signed layouts/modules; strict size/depth caps
- URL allowlist; restricted Wasm host API; CSP for WebViews
- Sensitive flows kept native-only
- Tooling
- Previewer, schema diffing, screenshot testing, property-based fuzzing
- CI gates; canary first; observability dashboards
- Performance
- Compression, patching, lazy load, style token resolution
- Device-class budgets for component counts and depth
- Observability
- LayoutId everywhere; decode/render metrics; outcome tracking
Conclusion
Server-Driven UI in 2025 can deliver on its promise—faster iteration without compromising safety—if you approach it as an engineering system, not a templating trick. The core ideas are straightforward: explicit schemas, negotiated capabilities, cautious behavior injection via Wasm, and mature rollout discipline. The hard work is in the guardrails, tooling, and culture of verification.
Adopt SDUI where it provides leverage: content-heavy surfaces, experiments, and seasonal promos. Keep critical, secure flows native. Invest early in schema governance, capability negotiation, and a preview pipeline that non-engineers can use. When you can flip a server flag and see a layout change safely propagate to millions of devices with screenshots, metrics, and a rollback switch, you’ll know you’ve crossed the threshold from risky experimentation to reliable platform.
References and further reading
- JSON Schema (2020-12): https://json-schema.org/
- WebAssembly Component Model overview: https://component-model.bytecodealliance.org/
- IETF: HTTP content negotiation concepts (RFC 9110)
- Shopify Engineering on Remote UI: https://shopify.engineering/remote-ui
- OWASP Mobile Security Testing Guide: https://owasp.org/www-project-mobile-security-testing-guide/
- Brotli compression: https://www.rfc-editor.org/rfc/rfc7932
- JSON Patch (RFC 6902): https://www.rfc-editor.org/rfc/rfc6902
Notes: Companies across commerce, media, and social have discussed variants of SDUI publicly over the last decade, especially for TV and highly experiment-driven mobile surfaces. The specifics vary, but the winning patterns are consistent: tight schemas, careful negotiation, strong safety rails, and great tooling.