SDUI Architecture Patterns: Building Scalable Server-Driven UI Systems
Building a server-driven UI system that survives production is about architecture, not just rendering JSON. Here are the 8 patterns that separate toy demos from production-grade SDUI.
In This Article
- Overview: SDUI Architectural Decisions
- Pattern 1: Component Registry
- Pattern 2: Schema-First Design
- Pattern 3: Action System
- Pattern 4: Data Binding & State Management
- Pattern 5: BYOC vs Provided Components
- Pattern 6: Versioning & Backward Compatibility
- Pattern 7: Fallback & Error Handling
- Pattern 8: Experimentation Integration
- How Pyramid Implements These Patterns
Overview: Why SDUI Architecture Matters
Every mobile team that builds a server-driven UI system faces the same inflection point: the first version works for one screen. Then you need a second screen. Then conditional logic. Then A/B testing. Then backward compatibility with three app versions in the wild.
This is where most SDUI implementations break. Not because the rendering engine fails — that part is usually straightforward. They break because the architecture wasn't designed for scale.
A well-architected SDUI system needs to answer several fundamental questions:
- How do clients discover and render components? You need a registry that maps JSON types to native views — and handles unknown types gracefully.
- How is the contract between server and client defined? Without a schema, your backend and mobile teams will drift apart within weeks.
- How do users interact with server-driven screens? Taps, form submissions, navigation — all need a structured action system.
- How does dynamic data flow into layouts? Variables, expressions, and reactive state are what make SDUI screens feel alive, not static.
- What happens when the server sends something the client doesn't understand? Fallbacks and graceful degradation keep your app from crashing on older versions.
- How do you run experiments on server-driven screens? The whole point of SDUI is agility — your experimentation system should be native to it.
In this guide, we'll walk through 8 architecture patterns that address these questions. Each pattern includes real code examples in Kotlin and Swift, architecture diagrams, and guidance on when to apply them.
Let's dive into each pattern.
Pattern 1: Component Registry
The Component Registry is the foundation of every SDUI system. It's the lookup table that maps a component type string from your JSON (like "text" or "product_card") to a native UI implementation on the client.
The pattern has three phases: register, discover, and render.
Register
At app startup, native components register themselves with the registry. This is where you declare which JSON types your client can handle:
// Kotlin — Component Registration
interface SDUIComponent {
val type: String
@Composable
fun Render(props: JsonObject, children: List<JsonNode>)
}
class ComponentRegistry {
private val components = mutableMapOf<String, SDUIComponent>()
private var fallback: SDUIComponent? = null
fun register(component: SDUIComponent) {
components[component.type] = component
}
fun registerFallback(component: SDUIComponent) {
fallback = component
}
fun resolve(type: String): SDUIComponent {
return components[type] ?: fallback
?: throw UnknownComponentException(type)
}
fun supportedTypes(): Set<String> = components.keys
}
// Swift — Component Registration
protocol SDUIComponent {
var type: String { get }
@ViewBuilder
func render(props: [String: Any], children: [JSONNode]) -> some View
}
class ComponentRegistry {
private var components: [String: any SDUIComponent] = [:]
private var fallback: (any SDUIComponent)?
func register(_ component: any SDUIComponent) {
components[component.type] = component
}
func resolve(_ type: String) -> any SDUIComponent {
return components[type] ?? fallback ?? PlaceholderComponent()
}
}
Discover
When a layout JSON arrives, the render engine walks the component tree and asks the registry: "Do you know how to render this type?" This is the discovery phase.
Render
The resolved component receives its props and children, then renders a fully native view. No WebViews, no interpretation — just native platform UI:
// Kotlin — Recursive Rendering
@Composable
fun SDUIRenderer(
node: JsonNode,
registry: ComponentRegistry
) {
val component = registry.resolve(node.type)
component.Render(
props = node.properties,
children = node.children
)
}
// Usage: the render engine walks the tree recursively
@Composable
fun SDUIScreen(layout: Layout, registry: ComponentRegistry) {
layout.rootNodes.forEach { node ->
SDUIRenderer(node, registry)
}
}
💡 Registry Best Practice
Keep your registry flat. Don't nest registries or use inheritance hierarchies — a simple Map<String, Component> is faster to look up, easier to debug, and simpler to extend. Most production systems have 30-80 registered component types.
Pattern 2: Schema-First Design
Without a schema, your SDUI system is a JSON guessing game. The server sends whatever it wants, the client parses whatever it can, and bugs hide in the gap between the two.
Schema-first design means you define your component contracts in a formal schema (JSON Schema, Protocol Buffers, or a custom DSL) before writing any rendering code. Then you generate types, validators, and documentation from that single source of truth.
Define Components via Schema
// JSON Schema for a Button component
{
"$id": "https://pyramidui.com/schemas/button/v1",
"type": "object",
"required": ["type", "properties"],
"properties": {
"type": { "const": "button" },
"properties": {
"type": "object",
"required": ["label"],
"properties": {
"label": { "type": "string" },
"style": {
"type": "string",
"enum": ["primary", "secondary", "ghost"],
"default": "primary"
},
"disabled": { "type": "boolean", "default": false },
"action": { "$ref": "#/definitions/Action" }
}
}
}
}
Generate Code from Schema
From this single schema definition, you can generate:
- Kotlin data classes for Android parsing and type-safe access
- Swift Codable structs for iOS decoding
- TypeScript interfaces for your backend layout builder
- Validation rules that run on both server (before sending) and client (after receiving)
- Documentation that stays in sync with the actual implementation
// Kotlin — Generated from schema
@Serializable
data class ButtonProps(
val label: String,
val style: ButtonStyle = ButtonStyle.PRIMARY,
val disabled: Boolean = false,
val action: Action? = null
)
enum class ButtonStyle {
@SerialName("primary") PRIMARY,
@SerialName("secondary") SECONDARY,
@SerialName("ghost") GHOST
}
// Swift — Generated from schema
struct ButtonProps: Codable {
let label: String
var style: ButtonStyle = .primary
var disabled: Bool = false
var action: Action? = nil
}
enum ButtonStyle: String, Codable {
case primary, secondary, ghost
}
✅ Why Schema-First Wins
Schema-first design eliminates an entire category of bugs: "the server sent X but the client expected Y." With generated types, these mismatches become compile-time errors instead of runtime crashes.
Schema Validation Pipeline
Validate layouts at multiple points in the pipeline:
| Validation Point | What It Catches | When It Runs |
|---|---|---|
| CI/CD | Schema changes that break existing clients | On every schema PR |
| Server-side | Invalid layouts before they reach clients | On layout publish |
| Client-side | Corrupted or tampered responses | On JSON parse |
Pattern 3: Action System
A server-driven UI without actions is just a fancy static page. The action system defines how users interact with SDUI screens: tapping buttons, submitting forms, navigating between screens, and triggering API calls.
A well-designed action system covers four categories: navigation, HTTP calls, state mutations, and event handling.
Action Types
// Kotlin — Action model
@Serializable
sealed class Action {
// Navigate to another screen or URL
@Serializable
@SerialName("navigate")
data class Navigate(
val destination: String,
val params: Map<String, String> = emptyMap(),
val transition: Transition = Transition.PUSH
) : Action()
// Make an HTTP request
@Serializable
@SerialName("http")
data class Http(
val url: String,
val method: HttpMethod = HttpMethod.POST,
val body: JsonObject? = null,
val onSuccess: Action? = null,
val onError: Action? = null
) : Action()
// Mutate local state
@Serializable
@SerialName("setState")
data class SetState(
val key: String,
val value: JsonElement
) : Action()
// Fire an analytics or custom event
@Serializable
@SerialName("event")
data class Event(
val name: String,
val properties: Map<String, String> = emptyMap()
) : Action()
// Chain multiple actions sequentially
@Serializable
@SerialName("sequence")
data class Sequence(
val actions: List<Action>
) : Action()
}
Action Handler
// Kotlin — Action execution engine
class ActionHandler(
private val navigator: Navigator,
private val httpClient: HttpClient,
private val stateStore: StateStore,
private val analytics: Analytics
) {
suspend fun handle(action: Action) {
when (action) {
is Action.Navigate -> {
navigator.navigate(action.destination, action.params)
}
is Action.Http -> {
try {
httpClient.request(action.url, action.method, action.body)
action.onSuccess?.let { handle(it) }
} catch (e: Exception) {
action.onError?.let { handle(it) }
}
}
is Action.SetState -> {
stateStore.set(action.key, action.value)
}
is Action.Event -> {
analytics.track(action.name, action.properties)
}
is Action.Sequence -> {
action.actions.forEach { handle(it) }
}
}
}
}
Action Composition in JSON
Actions compose naturally. Here's a "Add to Cart" button that fires an analytics event, makes an API call, and navigates on success:
{
"type": "button",
"properties": {
"label": "Add to Cart",
"action": {
"type": "sequence",
"actions": [
{
"type": "event",
"name": "add_to_cart_tapped",
"properties": { "productId": "{{product.id}}" }
},
{
"type": "http",
"url": "/api/cart/add",
"method": "POST",
"body": { "productId": "{{product.id}}" },
"onSuccess": {
"type": "navigate",
"destination": "/cart"
}
}
]
}
}
}
⚠️ Security Consideration
Never allow arbitrary URL execution from server-driven actions. Maintain an allowlist of permitted hosts and URL patterns. Validate every HTTP action against it before execution. See our SDUI Security Best Practices guide.
Pattern 4: Data Binding & State Management
Static layouts are easy. The real challenge is making SDUI screens dynamic: showing the user's name, updating a counter, toggling visibility based on state. This requires a data binding system that connects server-defined expressions to client-side state.
Variables & Expressions
Use a simple expression syntax (like {{variable}}) to bind data into layouts:
// JSON layout with data binding
{
"type": "screen",
"variables": {
"user": { "source": "api", "url": "/api/user/me" },
"cartCount": { "source": "local", "default": 0 }
},
"children": [
{
"type": "text",
"properties": {
"content": "Hello, {{user.firstName}}!"
}
},
{
"type": "badge",
"properties": {
"count": "{{cartCount}}",
"visible": "{{cartCount > 0}}"
}
}
]
}
Expression Engine
// Kotlin — Expression evaluator
class ExpressionEngine(
private val stateStore: StateStore
) {
private val pattern = Regex("""\{\{(.+?)\}\}""")
fun resolve(template: String): String {
return pattern.replace(template) { match ->
val expression = match.groupValues[1].trim()
evaluate(expression).toString()
}
}
fun resolveBoolean(template: String): Boolean {
val expr = pattern.find(template)?.groupValues?.get(1)?.trim()
?: return template.toBoolean()
return evaluate(expr) as? Boolean ?: false
}
private fun evaluate(expression: String): Any? {
// Handle comparisons: "cartCount > 0"
if (expression.contains(">")) {
val (left, right) = expression.split(">").map { it.trim() }
val leftVal = resolveVariable(left) as? Number ?: 0
val rightVal = right.toIntOrNull() ?: 0
return leftVal.toInt() > rightVal
}
// Handle dot notation: "user.firstName"
return resolveVariable(expression)
}
private fun resolveVariable(path: String): Any? {
return stateStore.get(path)
}
}
Reactive Updates
When state changes, the UI should automatically reflect those changes. This is where reactive state management comes in:
// Kotlin — Reactive state store
class StateStore {
private val state = mutableStateMapOf<String, Any?>()
fun set(key: String, value: Any?) {
state[key] = value
}
fun get(path: String): Any? {
val parts = path.split(".")
var current: Any? = state[parts.first()]
for (part in parts.drop(1)) {
current = (current as? Map<*, *>)?.get(part)
}
return current
}
// Compose automatically recomposes when state changes
@Composable
fun observe(path: String): State<Any?> {
return remember { derivedStateOf { get(path) } }
}
}
// Swift — Reactive state store
@Observable
class StateStore {
private var state: [String: Any] = [:]
func set(_ key: String, value: Any) {
state[key] = value
}
func get(_ path: String) -> Any? {
let parts = path.split(separator: ".")
var current: Any? = state[String(parts.first!)]
for part in parts.dropFirst() {
current = (current as? [String: Any])?[String(part)]
}
return current
}
}
💡 Keep Expressions Simple
Resist the temptation to build a full programming language in your expression engine. Stick to variable interpolation, simple comparisons, and boolean logic. If you need complex logic, move it to the server or to native code.
Pattern 5: BYOC vs Provided Components
One of the biggest architectural decisions in SDUI is: who provides the components? There are two approaches, and most production systems use a hybrid.
Provided Components (SDK Approach)
The SDUI framework ships with a pre-built library of components: buttons, text, images, lists, cards, forms. Teams use these directly.
| Pros | Cons |
|---|---|
| Instant setup — works out of the box | Limited to what the SDK offers |
| Consistent behavior across apps | May not match your design system |
| Built-in accessibility and theming | Harder to customize deeply |
| Less client-side code to maintain | Framework dependency for UI updates |
BYOC — Bring Your Own Components
Teams register their own native components and map them to server types. The SDUI framework handles orchestration, but the actual UI is yours.
// Kotlin — BYOC Registration
// Your custom component, your design system
class BrandedProductCard : SDUIComponent {
override val type = "product_card"
@Composable
override fun Render(props: JsonObject, children: List<JsonNode>) {
// Your design system components
MyDesignSystem.Card(
elevation = MyDesignSystem.Elevation.Medium
) {
ProductImage(url = props["imageUrl"].string)
ProductTitle(text = props["title"].string)
PriceTag(
price = props["price"].double,
currency = props["currency"].string
)
}
}
}
// Register at app startup
registry.register(BrandedProductCard())
| Pros | Cons |
|---|---|
| Full control over look and feel | More work to implement each component |
| Uses your existing design system | Must handle accessibility yourself |
| No framework lock-in on UI layer | Requires client-side code for new types |
| Pixel-perfect design compliance | Slower to add new component types |
The Hybrid Approach
The best production systems use both: a library of provided primitives (text, image, spacer, divider) plus BYOC for domain-specific components (product cards, user profiles, payment widgets).
// Register SDK primitives + custom components
fun setupRegistry(): ComponentRegistry {
val registry = ComponentRegistry()
// SDK-provided primitives
registry.register(SDKText())
registry.register(SDKImage())
registry.register(SDKButton())
registry.register(SDKColumn())
registry.register(SDKRow())
registry.register(SDKSpacer())
// Your custom BYOC components
registry.register(BrandedProductCard())
registry.register(CustomCheckoutWidget())
registry.register(LiveStreamPlayer())
// Fallback for anything unknown
registry.registerFallback(EmptyComponent())
return registry
}
Pattern 6: Versioning & Backward Compatibility
Here's the reality of mobile: when you push a schema change to your SDUI backend, app version 3.2.0 (released today) will understand it. But app version 3.0.0 (released two months ago and still running on 15% of devices) might not. Versioning is how you handle this.
Schema Versioning Strategies
There are three main approaches:
1. URL-based versioning: The simplest — different endpoints for different schema versions.
GET /api/v1/layout/home → Schema v1 (legacy clients)
GET /api/v2/layout/home → Schema v2 (current clients)
GET /api/v3/layout/home → Schema v3 (latest clients)
2. Header-based negotiation: More flexible — the client declares what it supports.
// Client sends supported schema version
GET /api/layout/home
X-Schema-Version: 2
X-Supported-Components: text,image,button,card,carousel
// Server responds with compatible layout
3. Embedded versioning: Each component carries its own version.
{
"schemaVersion": 2,
"children": [
{
"type": "carousel",
"version": 3,
"properties": { ... }
}
]
}
Deprecation Lifecycle
Don't remove old component types overnight. Follow a structured deprecation lifecycle:
// Kotlin — Version-aware component resolution
class VersionedRegistry {
private val components = mutableMapOf<String, Map<Int, SDUIComponent>>()
fun register(type: String, version: Int, component: SDUIComponent) {
val versions = components.getOrPut(type) { mutableMapOf() }
(versions as MutableMap)[version] = component
}
fun resolve(type: String, requestedVersion: Int): SDUIComponent? {
val versions = components[type] ?: return null
// Try exact version, then fall back to latest compatible
return versions[requestedVersion]
?: versions.filterKeys { it <= requestedVersion }
.maxByOrNull { it.key }?.value
}
}
⚠️ Rule of Thumb
Support at least the last 3 major app versions (or ~6 months of releases). Beyond that, prompt users to update. Track version distribution in analytics to know when it's safe to drop old schema versions.
Pattern 7: Fallback & Error Handling
In production, things go wrong. The server sends a component type your client doesn't know. The network drops mid-response. A layout references an image that 404s. Your SDUI architecture must handle all of this gracefully.
Unknown Component Handling
When the registry encounters a type it doesn't recognize, it should never crash. There are three strategies:
// Kotlin — Fallback strategies
enum class FallbackStrategy {
SKIP, // Silently skip the unknown component
PLACEHOLDER, // Render a placeholder box
NEAREST // Try to find a similar registered component
}
class ResilientRegistry(
private val strategy: FallbackStrategy = FallbackStrategy.SKIP
) {
fun resolve(type: String): SDUIComponent {
components[type]?.let { return it }
// Log unknown type for monitoring
analytics.track("sdui_unknown_component", mapOf(
"type" to type,
"appVersion" to BuildConfig.VERSION_NAME
))
return when (strategy) {
FallbackStrategy.SKIP -> EmptyComponent()
FallbackStrategy.PLACEHOLDER -> PlaceholderComponent(type)
FallbackStrategy.NEAREST -> findNearest(type) ?: EmptyComponent()
}
}
}
Network Failure Resilience
// Swift — Graceful degradation on network failure
class SDUIScreenLoader {
private let cache: SDUICache
private let api: SDUIApiClient
private let bundledLayouts: [String: Layout]
func loadScreen(_ route: String) async -> Layout {
// Strategy 1: Try network
if let fresh = try? await api.fetchLayout(route) {
cache.store(route, layout: fresh)
return fresh
}
// Strategy 2: Fall back to cache
if let cached = cache.get(route) {
return cached
}
// Strategy 3: Fall back to bundled layout
if let bundled = bundledLayouts[route] {
return bundled
}
// Strategy 4: Show error screen (never blank)
return Layout.errorScreen(
title: "Something went wrong",
retryAction: Action.navigate(route)
)
}
}
The Fallback Cascade
✅ Golden Rule: Never Show a Blank Screen
Even in the worst case — no network, empty cache, no bundled layout — show something. An error screen with a retry button is infinitely better than a white screen or a crash. Your users will forgive stale data. They won't forgive a frozen app.
Pattern 8: Experimentation Integration
SDUI and experimentation are natural partners. Since the server controls the UI, A/B testing a layout change is as simple as returning a different JSON response. No app release required. No feature flags. No code changes on the client.
Automatic Exposure Tracking
When a user sees an experimental layout, you need to track that exposure automatically — not rely on engineers remembering to add analytics calls.
// Kotlin — Automatic experiment exposure tracking
class ExperimentAwareLayoutService(
private val layoutService: LayoutService,
private val experimentService: ExperimentService,
private val analytics: Analytics
) {
suspend fun getLayout(
route: String,
userId: String,
context: RequestContext
): LayoutResponse {
// Check if this route has active experiments
val experiments = experimentService.getActiveExperiments(route)
if (experiments.isEmpty()) {
return layoutService.getLayout(route)
}
// Assign user to variant
val assignments = experiments.map { exp ->
val variant = experimentService.assign(userId, exp)
ExperimentAssignment(exp.id, variant.id)
}
// Fetch the variant-specific layout
val layout = layoutService.getLayout(route, assignments)
// Return layout with experiment metadata
return LayoutResponse(
layout = layout,
experiments = assignments
)
}
}
// Client-side: track exposure when layout is rendered
@Composable
fun ExperimentTrackedScreen(response: LayoutResponse) {
LaunchedEffect(response.experiments) {
response.experiments.forEach { assignment ->
analytics.track("experiment_exposure", mapOf(
"experimentId" to assignment.experimentId,
"variantId" to assignment.variantId
))
}
}
SDUIScreen(response.layout)
}
A/B Testing Layouts
Here's what A/B testing looks like when the server controls the UI. The same endpoint returns different layouts based on experiment assignment:
// Server-side layout resolution (pseudo-code)
fun resolveLayout(route: String, userId: String): Layout {
val experiment = experiments.get("homepage_redesign_2026")
return when (experiment.assignVariant(userId)) {
"control" -> {
// Original layout: hero → categories → products
Layout(
children = listOf(heroSection, categoryGrid, productList)
)
}
"variant_a" -> {
// Variant A: personalized feed → hero → categories
Layout(
children = listOf(personalizedFeed, heroSection, categoryGrid)
)
}
"variant_b" -> {
// Variant B: search-first → trending → categories
Layout(
children = listOf(searchBar, trendingSection, categoryGrid)
)
}
}
}
Experiment Metadata in JSON
// Layout response with experiment metadata
{
"route": "/home",
"experiments": [
{
"id": "homepage_redesign_2026",
"variant": "variant_a",
"trackExposure": true
}
],
"layout": {
"type": "screen",
"children": [...]
}
}
💡 SDUI Superpower: Zero-Deploy A/B Tests
With traditional native apps, running an A/B test on a UI layout requires: code the variant, ship a new build, wait for app review, wait for adoption. With SDUI, you change a JSON response and the experiment is live in seconds. No build. No review. No adoption lag.
How Pyramid Implements These Patterns
Pyramid was designed from the ground up with these 8 patterns as first-class concerns — not afterthoughts.
Component Registry
Pyramid ships with a hybrid registry: 40+ built-in primitives (text, image, button, form fields, lists, grids) plus a BYOC API for custom components. Registration is a single function call, and the fallback behavior is configurable per-screen.
Schema-First
Every Pyramid component is defined in a versioned JSON Schema. The Pyramid CLI generates Kotlin data classes and Swift Codable structs from these schemas, eliminating an entire class of serialization bugs. Schema validation runs on our cloud before layouts reach your app.
Action System
Pyramid's built-in action system supports navigation, HTTP calls, state mutations, analytics events, and action composition (sequences and conditionals). An action allowlist is enforced client-side to prevent unauthorized URL execution.
Data Binding
Pyramid supports {{variable}} expressions with dot-notation path resolution, comparisons, and boolean logic. Variables can be sourced from API responses, local storage, or the action system. State changes trigger reactive updates — the binding layer is integrated directly with Jetpack Compose's state and SwiftUI's @Observable.
Versioning
Pyramid uses header-based negotiation. The SDK sends its schema version and supported component list. The Pyramid cloud returns a compatible layout, automatically downgrading components that the client can't render to their closest supported equivalent.
Error Handling
Pyramid implements the full fallback cascade: network → cache → bundled → error screen. The SDK bundles a set of default layouts for critical screens, so even first-launch-offline works. Unknown components are silently skipped in production and visually flagged in debug builds.
Experimentation
Experiment assignment happens at the layout service level. Pyramid tracks exposure automatically when a variant layout is rendered. Integrate with your existing experimentation platform (LaunchDarkly, Statsig, Eppo) or use Pyramid's built-in experiment runner for layout-level A/B tests.
Build on Solid Architecture
Pyramid implements all 8 patterns out of the box — component registry, schema-first design, action system, data binding, BYOC, versioning, fallbacks, and experimentation. Stop reinventing the wheel.
Join the Waitlist →Pattern Summary
| Pattern | What It Solves | Complexity |
|---|---|---|
| Component Registry | Mapping JSON types to native views | Low |
| Schema-First Design | Server/client contract drift | Medium |
| Action System | User interactions on dynamic screens | Medium |
| Data Binding | Dynamic content in static layouts | Medium-High |
| BYOC vs Provided | Design system integration | Low (decision) |
| Versioning | Multi-version app compatibility | High |
| Fallback & Errors | Production resilience | Medium |
| Experimentation | A/B testing without deploys | Medium |
What to Build First
If you're building an SDUI system from scratch, here's the order we recommend:
- Component Registry + Fallback — This is your foundation. Nothing works without it.
- Schema-First Design — Define your contracts early. It's 10x harder to retrofit schemas later.
- Action System — Without actions, your screens are read-only.
- Data Binding — This is what makes screens feel dynamic and personalized.
- Versioning — Critical once you have multiple app versions in the wild.
- Experimentation — The payoff that makes the whole investment worthwhile.
Or skip the build phase entirely and start with Pyramid, which implements all eight patterns on day one.
Skip the Build Phase
These 8 patterns represent months of architecture and engineering. Pyramid gives you all of them on day one, so you can focus on building product instead of infrastructure.
Get Early Access →Further Reading
Related Articles
- Server-Driven UI for Android: A Complete Implementation Guide →
- Getting Started with Pyramid: Add Server-Driven UI to Your Android App in 30 Minutes →
- Dynamic Onboarding Flows Without App Releases →
- Generative UI Is the Future — Here's Why SDUI Is the Foundation →
- Why Airbnb, Lyft, and Netflix Use Server-Driven UI →
- How Nubank Uses SDUI to Ship Features to 115M Customers in Minutes →
- Why JSON-Based SDUI Breaks at Scale (And What Comes Next) →
- GraphQL + SDUI: The Architecture Guide →