How to Migrate to Server-Driven UI: A Step-by-Step Guide for Mobile Teams
You don't have to rewrite your app to adopt SDUI. The best migrations are incremental — starting with config flags and progressing to full server control. This guide walks you through every level, with code examples, timelines, and lessons from teams who've done it.
1. Why Migrate to Server-Driven UI?
Before we get into the how, let's be honest about the why. Server-driven UI migration is a significant investment. You need a clear-eyed understanding of the problem you're solving.
The Release Bottleneck
Mobile teams face a fundamental constraint that web teams don't: every UI change requires an app store release. That means:
- 24-48 hours minimum for App Store review (sometimes longer)
- Days to weeks of adoption lag — even after approval, users don't update immediately
- Coordinated release trains where a small copy change gets bundled with a major feature because "we might as well"
- Hotfix anxiety — a typo in production means an emergency release cycle
Your web team changes a button label in 5 minutes. Your mobile team needs a sprint to do the same thing safely.
The Business Case
This isn't just a developer experience problem. Slow releases have measurable business impact:
- Experiment velocity: Lyft reported that server-driven experiments take 1-2 days vs. 2+ weeks for client-driven ones. Fewer experiments = fewer learnings = slower growth.
- Time-sensitive updates: Promotions, seasonal campaigns, and regulatory changes can't wait for a release train.
- User fragmentation: With 30% of users on app versions older than 6 months, hardcoded UI means you can't reach everyone.
- Engineering cost: Mobile engineers spend an estimated 20-30% of their time on release process overhead — building, testing, coordinating, and waiting.
When NOT to Migrate
To be fair, SDUI migration isn't the right move for every team:
- Your app UI rarely changes (maybe a utility app or calculator)
- You have a team of 1-2 engineers who can't spare the upfront investment
- Your app is performance-critical in ways that don't tolerate any rendering overhead (games, video editors)
- You're pre-product-market-fit and the architecture itself is still shifting weekly
If none of those apply and you're feeling the release bottleneck, keep reading.
2. Prerequisites Assessment
Before writing any migration code, do an honest assessment. Teams that skip this phase end up building the wrong thing.
Team Readiness Checklist
- Design system exists: Do you have a consistent component library? SDUI works best when your UI is already built from reusable components. If every screen is a snowflake, componentize first.
- Backend team buy-in: SDUI shifts work to the server. If your backend team sees this as "not my problem," the migration will stall. Get alignment early.
- CI/CD maturity: You need reliable builds and deploys on the backend side. If deploying a server change is also painful, you're just moving the bottleneck.
- Monitoring: Server-driven means server-dependent. You need visibility into latency, error rates, and fallback triggers before going to production.
Architecture Audit
Evaluate your current codebase with these questions:
- How are screens structured? Are they monolithic activities/view controllers, or do they compose smaller reusable components? Component-based architectures (Compose, SwiftUI) are easier to migrate.
- Where does data flow? Is there a clear separation between data fetching and UI rendering? If your API responses already describe "what to show" (not just raw data), you're closer than you think.
- Which screens change most? Audit your git history. The screens with the most commits in the last 6 months are your migration candidates.
- What's your offline story? If your app needs to work without network, plan your caching strategy before starting.
3. The 5 Migration Levels
SDUI migration isn't binary — it's a spectrum. We break it into 5 maturity levels, each delivering incremental value. You don't have to reach Level 4 to benefit. Most teams get massive ROI at Level 2.
| Level | Name | What Changes | Who Benefits |
|---|---|---|---|
| 0 | Config Flags | Feature toggles, simple values | Engineering |
| 1 | Dynamic Content | Text, images, ordering | Product + Marketing |
| 2 | Dynamic Layouts | Component composition | Product + Design |
| 3 | Full SDUI | Entire screens from server | Entire org |
| 4 | Zero-Release | Visual editor, A/B testing, no code | Business + Growth |
Let's walk through each level with concrete code examples.
4. Level 0: Config Flags
Level 0 Remote Configuration
Control feature visibility and simple values from your server. This is the gentlest on-ramp — you're probably already doing some version of this with Firebase remote config or LaunchDarkly.
At Level 0, your layouts remain hardcoded. The server only controls which features are visible and simple configuration values like strings and colors. The key distinction from a feature flag service: you own the contract and can evolve it toward SDUI.
Kotlin / Jetpack Compose
// Level 0: Server-controlled configuration
@Serializable
data class ScreenConfig(
val showPromoBanner: Boolean = false,
val promoBannerText: String = "",
val ctaButtonLabel: String = "Get Started",
val maxItemsToShow: Int = 10
)
@Composable
fun HomeScreen(config: ScreenConfig) {
Column {
if (config.showPromoBanner) {
PromoBanner(text = config.promoBannerText)
}
ProductList(maxItems = config.maxItemsToShow)
Button(onClick = { /* navigate */ }) {
Text(config.ctaButtonLabel)
}
}
}
Swift / SwiftUI
// Level 0: Server-controlled configuration
struct ScreenConfig: Codable {
var showPromoBanner: Bool = false
var promoBannerText: String = ""
var ctaButtonLabel: String = "Get Started"
var maxItemsToShow: Int = 10
}
struct HomeScreen: View {
let config: ScreenConfig
var body: some View {
VStack {
if config.showPromoBanner {
PromoBanner(text: config.promoBannerText)
}
ProductList(maxItems: config.maxItemsToShow)
Button(config.ctaButtonLabel) {
// navigate
}
}
}
}
What you gain: Toggle features without releases. Change copy. Run simple on/off experiments. It's limited, but it's a foundation.
Key architectural decision: Even at Level 0, define your config endpoint and response format intentionally. This will evolve into your SDUI schema. Don't just use Firebase Remote Config as a string-to-string dictionary — structure it.
5. Level 1: Dynamic Content
Level 1 Server-Driven Content
The server controls what content appears, but layouts stay native. Think: section titles, image URLs, item ordering, and content blocks — not component structure.
At Level 1, you move from "the server tells us which features are on" to "the server tells us what content to show." Layouts are still hardcoded, but the server controls text, images, ordering, and which content blocks appear.
Kotlin / Jetpack Compose
// Level 1: Server controls content, client controls layout
@Serializable
data class HomeContent(
val heroTitle: String,
val heroImageUrl: String,
val sections: List<ContentSection>
)
@Serializable
data class ContentSection(
val id: String,
val title: String,
val type: String, // "product_grid", "banner", "text_block"
val items: List<ContentItem> = emptyList(),
val visible: Boolean = true
)
@Composable
fun HomeScreen(content: HomeContent) {
LazyColumn {
item {
HeroSection(
title = content.heroTitle,
imageUrl = content.heroImageUrl
)
}
items(content.sections.filter { it.visible }) { section ->
when (section.type) {
"product_grid" -> ProductGrid(section)
"banner" -> PromoBanner(section)
"text_block" -> TextBlock(section)
else -> { /* skip unknown types */ }
}
}
}
}
What you gain: Marketing can change homepage content without releases. You can reorder sections, swap hero images, and update copy server-side. This is where product and marketing teams start to feel the benefit.
The critical shift: Notice the when (section.type) pattern. This is the seed of a component registry pattern. At Level 1 it's a switch statement; at Level 3 it becomes a proper registry.
6. Level 2: Dynamic Layouts
Level 2 Server-Driven Layouts
The server defines which components appear, in what order, and with what properties. You're now composing screens from a component vocabulary, not just filling in content slots.
Level 2 is where the real SDUI migration begins. Instead of the client deciding "hero goes here, then grid, then banner," the server sends a list of components and the client renders them in order. The server controls composition.
Kotlin / Jetpack Compose
// Level 2: Server defines component composition
@Serializable
data class UIComponent(
val type: String,
val props: Map<String, JsonElement> = emptyMap(),
val children: List<UIComponent> = emptyList()
)
// Component registry — maps types to renderers
class ComponentRegistry {
private val renderers = mutableMapOf<String, ComponentRenderer>()
fun register(type: String, renderer: ComponentRenderer) {
renderers[type] = renderer
}
fun resolve(type: String): ComponentRenderer? = renderers[type]
}
typealias ComponentRenderer = @Composable (
props: Map<String, JsonElement>,
children: List<UIComponent>
) -> Unit
// Register your existing design system components
val registry = ComponentRegistry().apply {
register("HeroBanner") { props, _ ->
HeroBanner(
title = props["title"]?.asString() ?: "",
imageUrl = props["imageUrl"]?.asString() ?: ""
)
}
register("ProductCarousel") { props, _ ->
ProductCarousel(
categoryId = props["categoryId"]?.asString() ?: "",
title = props["title"]?.asString() ?: ""
)
}
register("InfoCard") { props, _ ->
InfoCard(
text = props["text"]?.asString() ?: "",
style = props["style"]?.asString() ?: "default"
)
}
}
// Render any screen from server response
@Composable
fun DynamicScreen(components: List<UIComponent>) {
LazyColumn {
items(components) { component ->
val renderer = registry.resolve(component.type)
if (renderer != null) {
renderer(component.props, component.children)
}
// Unknown types are silently skipped — graceful degradation
}
}
}
Swift / SwiftUI
// Level 2: Server defines component composition
struct UIComponent: Codable, Identifiable {
let id: String
let type: String
let props: [String: AnyCodable]
let children: [UIComponent]
}
struct DynamicScreen: View {
let components: [UIComponent]
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(components) { component in
resolveComponent(component)
}
}
}
}
@ViewBuilder
func resolveComponent(_ component: UIComponent) -> some View {
switch component.type {
case "HeroBanner":
HeroBanner(
title: component.props["title"]?.stringValue ?? "",
imageURL: component.props["imageUrl"]?.stringValue ?? ""
)
case "ProductCarousel":
ProductCarousel(
categoryId: component.props["categoryId"]?.stringValue ?? "",
title: component.props["title"]?.stringValue ?? ""
)
default:
EmptyView() // graceful fallback
}
}
}
What you gain: The server can now rearrange your home screen, add new sections, remove them, personalize per user — all without a release. This is where most teams say "oh, this is what the fuss is about."
7. Level 3: Full SDUI
Level 3 Full Server-Driven UI
Complete screens are defined on the server — layout trees, nested components, actions, state management. The client is a generic rendering engine.
Level 3 is where you build a true SDUI system. The server sends a complete component tree — not just a flat list, but nested components with layout containers, styling properties, and action handlers. The client becomes a general-purpose renderer.
The Server Response
// Full SDUI server response — a complete screen definition
{
"screen": "home",
"root": {
"type": "ScrollView",
"children": [
{
"type": "Column",
"props": { "padding": 16, "spacing": 12 },
"children": [
{
"type": "Text",
"props": {
"text": "Welcome back, Sarah",
"style": "heading1"
}
},
{
"type": "Card",
"props": { "elevation": 2 },
"children": [
{
"type": "Image",
"props": { "url": "https://...", "aspectRatio": 1.5 }
},
{
"type": "Button",
"props": { "text": "Shop Now", "style": "primary" },
"actions": {
"onTap": { "type": "navigate", "route": "/products/summer" }
}
}
]
}
]
}
]
}
}
The Recursive Renderer (Kotlin)
@Composable
fun SDUIRenderer(
node: UIComponent,
registry: ComponentRegistry,
actionHandler: ActionHandler
) {
val renderer = registry.resolve(node.type)
if (renderer != null) {
renderer(node.props, node.children)
} else {
// Fallback: render children if we don't recognize the parent
node.children.forEach { child ->
SDUIRenderer(child, registry, actionHandler)
}
}
}
// Register layout containers that render children recursively
registry.register("Column") { props, children ->
Column(
modifier = Modifier
.padding(props.dpOrDefault("padding", 0)),
verticalArrangement = Arrangement.spacedBy(
props.dpOrDefault("spacing", 0)
)
) {
children.forEach { child ->
SDUIRenderer(child, registry, actionHandler)
}
}
}
registry.register("Row") { props, children ->
Row(
modifier = Modifier
.padding(props.dpOrDefault("padding", 0)),
horizontalArrangement = Arrangement.spacedBy(
props.dpOrDefault("spacing", 0)
)
) {
children.forEach { child ->
SDUIRenderer(child, registry, actionHandler)
}
}
}
Action System
// Actions connect UI events to behavior
class ActionHandler(
private val navigator: Navigator,
private val analytics: Analytics,
private val apiClient: ApiClient
) {
fun handle(action: Action) {
when (action.type) {
"navigate" -> navigator.navigateTo(action.route)
"deeplink" -> navigator.openDeepLink(action.url)
"api_call" -> apiClient.execute(action.endpoint, action.params)
"track" -> analytics.track(action.event, action.properties)
"set_state" -> stateManager.update(action.key, action.value)
}
}
}
What you gain: Any screen can be defined, modified, or replaced from the server. New features can ship without mobile releases. Personalization becomes trivial — send different component trees to different users.
What to watch for: At Level 3, testing complexity increases significantly. You need schema validation, contract tests between server and client, and snapshot testing for rendered output. Don't skip this.
8. Level 4: Zero-Release
Level 4 Zero-Release Operations
Visual editing tools, built-in A/B testing, analytics integration, and role-based access. Non-engineers can create and modify screens without writing code or waiting for releases.
Level 4 is where SDUI goes from an engineering tool to an organizational capability. At this level, product managers can build screens, growth teams can run experiments, and marketing can update promotions — all without filing a ticket.
This is what why companies like Airbnb use SDUI's Ghost Platform, Lyft's Canvas, and DoorDash's Mosaic enable internally. It's also what takes the longest to build from scratch.
What Level 4 Looks Like
- Visual editor: Drag-and-drop screen builder that outputs your SDUI schema
- A/B testing engine: Define variants, set traffic splits, measure conversion — all from a dashboard
- Preview system: See exactly what a screen looks like on device before publishing
- Rollback: Instantly revert any screen to a previous version
- Role-based access: Engineers register components, PMs compose screens, marketing edits copy
- Analytics integration: Automatic event tracking for server-driven screens
"The time it takes to build and roll out a server-driven experiment can be as few as a day or two, whereas client-driven experiments require a minimum of 2 weeks."
— Lyft Engineering
The honest truth: Building Level 4 yourself takes 6-12 months of dedicated platform engineering. That's why most companies that achieve this level either have massive platform teams (Airbnb, Netflix) or use a managed platform.
9. Common Migration Pitfalls
We've seen teams fail at SDUI migration in predictable ways. Here are the patterns to avoid:
❌ Pitfall 1: Trying to Migrate Everything at Once
The "big bang" approach — converting every screen to SDUI simultaneously — almost always fails. It creates a massive blast radius, makes debugging impossible, and burns team goodwill.
Instead: Start with one screen. Prove it works. Expand incrementally. Run SDUI and traditional screens side by side — they coexist fine.
❌ Pitfall 2: No Fallback Strategy
Server-driven means server-dependent. If your API is down and you have no fallback, your users see a blank screen. This is worse than hardcoded UI.
Instead: Always ship a bundled fallback for critical screens. Cache the last successful response. Implement graceful degradation for unknown component types.
❌ Pitfall 3: Ignoring Offline Support
Many SDUI implementations work great with network and break completely without it. If your users ever have spotty connectivity (subway, rural areas, flights), this is a deal-breaker.
Instead: Cache UI definitions aggressively. Use stale-while-revalidate patterns. For critical flows (checkout, authentication), have hardcoded fallbacks.
❌ Pitfall 4: Over-Engineering the Schema
Teams spend months designing the "perfect" schema before rendering a single component. Then they discover it doesn't work for real screens.
Instead: Start with 5-10 component types. Build them for your pilot screen. Iterate the schema based on real usage, not theoretical completeness.
❌ Pitfall 5: Backend Team Not On Board
SDUI shifts significant responsibility to the backend. If backend engineers see it as "doing mobile's job" and push back, adoption stalls.
Instead: Involve backend from day one. Frame it as a platform investment, not a mobile convenience feature. The backend team needs to own the SDUI API as a product.
❌ Pitfall 6: No Versioning Strategy
You ship a new component type but users on old app versions don't know how to render it. Without versioning, every schema change risks breaking older clients.
Instead: Include client version in every request. Let the server respond with components the client can actually render. Unknown types should degrade gracefully (skip, show placeholder), never crash.
10. Real-World Migration Examples
Let's look at how real companies approached their SDUI migrations and what they learned.
Faire: 90% Less Rendering Code
Faire, the wholesale marketplace, migrated to SDUI and reported one of the most dramatic results in the industry: they eliminated approximately 90% of their client-side rendering logic.
Their approach was methodical:
- Started with their product browse experience — a high-change surface
- Built a component library mapped to their existing design system
- Gradually moved screens from hardcoded to server-driven
- Backend teams could now ship UI changes independently of mobile releases
The key lesson from Faire: SDUI doesn't add complexity — it moves it. The rendering logic didn't disappear; it moved to the server where it was easier to iterate, test, and deploy.
Airbnb: Ghost Platform
Airbnb's Ghost Platform is the most well-documented SDUI migration in the industry. Their approach started in 2017 and evolved over several years:
- Phase 1: Search results — the highest-traffic screen with the most experiments
- Phase 2: Listing pages — complex layouts that varied by property type
- Phase 3: Checkout and booking flows
- Phase 4: Eventually, most consumer-facing screens
"What if clients didn't need to know they were even displaying a listing? What if we could pass the UI directly to the client and skip the idea of listing data entirely?"
— Ryan Brooks, Airbnb
Airbnb's biggest insight: SDUI is most valuable where business logic intersects with UI presentation. A listing looks different in search results vs. wishlist vs. booking confirmation. Instead of encoding that logic on every client, they centralized it on the server.
DoorDash: Mosaic
DoorDash built Mosaic to power both their consumer and merchant apps. Their migration strategy focused on:
- Incremental adoption: Individual teams could opt into SDUI screen by screen
- Shared component library: The same components worked across consumer and merchant apps
- Experiment velocity: Growth teams could test new layouts without mobile engineering involvement
DoorDash's key lesson: make adoption optional and easy. Teams that were forced to adopt SDUI resisted; teams that could choose to adopt it became advocates.
11. How Pyramid Accelerates Migration
The companies above spent years and dozens of engineers building their SDUI platforms. That's realistic for a company with 500+ engineers and a dedicated platform team. For everyone else, it's not feasible.
Pyramid was built to give every mobile team the same capabilities without the multi-year investment. Here's how:
BYOC: Bring Your Own Components
This is the critical difference. Pyramid doesn't replace your design system — it renders your existing components server-side.
// Your existing Compose component — unchanged
@Composable
fun ProductCard(
title: String,
price: String,
imageUrl: String,
onTap: () -> Unit
) {
// Your existing implementation stays exactly the same
Card(onClick = onTap) {
AsyncImage(model = imageUrl)
Text(title, style = MaterialTheme.typography.titleMedium)
Text(price, style = MaterialTheme.typography.bodyLarge)
}
}
// Register it with Pyramid — one line
pyramid.register("ProductCard", ::ProductCard)
Your components. Your design system. Your codebase. Pyramid just lets the server compose them.
What Pyramid Gives You
- SDK for iOS and Android: Drop in the SDK, register your components, and start rendering server-driven screens
- Schema + API: A battle-tested component schema so you don't spend months designing your own
- Visual editor: A web-based tool for composing screens from your registered components — no code needed
- A/B testing: Built-in experimentation with traffic splitting, metrics, and winner deployment
- Fallbacks + caching: Offline support, graceful degradation, and automatic fallback handling built into the SDK
- Versioning: Client version awareness so old app versions never see components they can't render
Migration with Pyramid
With Pyramid, you can skip straight to Level 2 or Level 3 in weeks instead of months:
- Week 1: Install SDK, register 5-10 existing components
- Week 2: Build your pilot screen in the visual editor
- Week 3: A/B test the server-driven version against the hardcoded version
- Week 4: Roll out, monitor, and start the next screen
No custom schema design. No building a visual editor from scratch. No spending 6 months on infrastructure before seeing value.
12. Timeline Estimates
Here's what realistic timelines look like, based on a mid-sized mobile team (4-8 engineers) building from scratch vs. using a platform like Pyramid:
| Level | DIY (From Scratch) | With Pyramid | Value Delivered |
|---|---|---|---|
| Level 0 — Config Flags | 1-2 weeks | N/A (skip) | Feature toggles, simple A/B |
| Level 1 — Dynamic Content | 2-4 weeks | 1 week | Server-controlled copy & images |
| Level 2 — Dynamic Layouts | 4-8 weeks | 2 weeks | Server-composed screens |
| Level 3 — Full SDUI | 2-4 months | 3-4 weeks | Complete server-driven screens |
| Level 4 — Zero-Release | 6-12 months | 4-6 weeks | Visual editor, A/B, no-code |
Recommended Migration Pace
- Month 1: Pilot screen at Level 2. Prove value with a real screen that stakeholders can see.
- Month 2-3: Expand to 3-5 screens. Build confidence. Train the team.
- Month 4-6: Move to Level 3 for high-value screens. Establish patterns.
- Month 6+: Unlock Level 4 capabilities. Onboard non-engineering teams.
Don't rush. A well-executed Level 2 migration delivers more value than a rushed Level 4 that nobody trusts.
13. Getting Started
Here's a practical checklist for starting your SDUI migration today:
Ready to start your SDUI migration?
Pyramid gives you a production-ready SDUI platform with native SDKs, a visual editor, and A/B testing — without months of infrastructure work. Keep your components. ship faster.
Get Early Access →Related Articles
Conclusion
Migrating to server-driven UI isn't a weekend project, but it doesn't have to be a multi-year odyssey either. The key is thinking in levels — start with config flags, progress to dynamic content, then dynamic layouts, and eventually full SDUI. Each level delivers value on its own.
The companies that have done this successfully — Faire, Airbnb, DoorDash, Lyft — all share one insight: migrate incrementally, prove value early, and expand based on results. None of them rewrote their entire app on day one.
Whether you build your SDUI system from scratch or use a platform like Pyramid, the important thing is to start. Pick one screen. Register your components. Let the server drive it. See what happens.
Your web team has been shipping at this speed for years. It's time your mobile team caught up.