Server-Driven UI Setup for Mobile Apps
Server-Driven UI is an architectural pattern where the server sends not just data, but a description of how to display it. The mobile client is a universal renderer that dynamically builds screens from JSON schemas. Airbnb uses this in Ghost Platform, Airbnb Lottie, and Epoxy. Mercado Libre applies it to most app screens.
Why It's More Complex Than It Looks
At first glance: "send JSON describing the screen—client renders it." In practice, it's a full runtime and UI engine requiring design, versioning, testing, and maintenance.
Schema versioning. Users update apps slowly. If server sends "type": "new_component" but client version 2.1 doesn't know it—the screen crashes or renders incorrectly. Need a strategy: fallback component, minimum supported version, graceful degradation.
Type safety and validation. Without strict schema (JSON Schema, Protobuf, kotlinx.serialization with sealed classes), server can send invalid payload and crash the client instead of server. On iOS—Codable with DecodingStrategy.useDefaultValues; on Android—@SerialName + sealed class with @JsonClassDiscriminator.
Parsing performance. Complex screen in JSON—5–50 KB. Not recalculated per scroll, but cold render on first open plus schema caching is its own engineering task.
SDUI System Architecture
Server-side
Server stores component tree for each screen. Structure:
{
"version": "1.0",
"screen": {
"type": "scroll_view",
"children": [
{
"type": "hero_banner",
"props": {
"imageUrl": "https://cdn.example.com/banner.jpg",
"title": "Summer Sale",
"action": { "type": "navigate", "route": "/sale" }
}
},
{
"type": "product_grid",
"props": {
"columns": 2,
"dataSource": { "endpoint": "/api/products/featured" }
}
}
]
}
}
Component system—type registry: hero_banner, product_grid, text_block, cta_button, carousel. New component type added to server and client simultaneously, deployed via App Store/Play Store with new app version.
Client-side (iOS—SwiftUI)
// Component Registry—registry of all known types
enum ComponentType: String, Codable {
case heroBanner = "hero_banner"
case productGrid = "product_grid"
case unknown
}
// Renderer—recursive tree rendering
@ViewBuilder
func renderComponent(_ component: SDUIComponent) -> some View {
switch component.type {
case .heroBanner:
HeroBannerView(props: component.props)
case .productGrid:
ProductGridView(props: component.props)
default:
// Graceful fallback for unknown types
EmptyView()
}
}
On Android—similar via Jetpack Compose with when (component.type). Flutter—switch on type with factory method per widget.
When SDUI Is Justified
- Frequent layout changes without app release (marketing banners, promo screens, onboarding)
- A/B tests at screen level: server sends different schemas to different user segments
- Multiple brands in one app: white-label where each customer sees their screen
- Strong editorial team: CMS interface lets non-technical staff modify screens
When SDUI isn't needed: stable UI app, small team, no business requirement to change screens without App Store release.
Real Case
Retail app, iOS + Android. Marketing team wanted weekly main screen changes (banners, categories, recommendations)—without waiting 2–3 weeks for App Store review. SDUI implemented incrementally: first just main screen (Hero Banner + Category Grid), three months later—category pages and product cards. Backend—Laravel CMS with visual schema editor. Client—iOS SwiftUI, Android Compose. After 4 months, marketing independently publishes new screens without mobile developer involvement.
Infrastructure and CDN
Screen schemas cached on CDN (CloudFront, Fastly) with Cache-Control: max-age=300. Invalidation via stale-while-revalidate—users see cached version immediately, fresh version loads in background. For critical changes—Surrogate-Key for precise invalidation of specific screens.
Timeline: basic SDUI system (3–5 component types, one screen)—4–6 weeks. Full platform with CMS editor, A/B testing, schema versioning—12–20 weeks.







