Product Engineering

Server-Driven UI for Mobile Apps: How to Ship Screen Changes Without Waiting for the App Store

Every mobile team knows the pain: a small change to the home screen waits days for review and weeks for users to update. Server-Driven UI moves layout decisions to the backend. Here is how it works, where it pays off and where it does not.

Server-Driven UI (SDUI) is an approach where the backend decides what a mobile screen contains and the app simply renders it. Instead of hard-coding the home feed, the app asks the server, "What should I show?", receives a structured description of components, and draws each one using native building blocks it already ships with.

For product leaders, the appeal is changing a screen without a new app release. For engineers, it is a serious architectural commitment.

Why product teams adopt Server-Driven UI

A traditional app bakes layout into the binary. Every change means a build, a store review and waiting for users to update. Some never do.

SDUI changes the economics of that loop:

  • Faster iteration. Reorder a feed, launch a seasonal banner or retire a promotion from the backend.
  • Experimentation. Serve different layouts to different cohorts and measure the result, without shipping variant code paths to everyone.
  • Personalisation. The server already knows the user, their location and their history, so it can compose a screen for them.
  • Consistency. One response can describe the same screen for iOS, Android and web, so platforms stop drifting apart.

This is not a fringe idea. Airbnb has written publicly about its Ghost Platform, a server-driven UI system where a single backend response controls layout, section arrangement, data and actions across web, iOS and Android. Lyft's Bikes and Scooters team has described its own move to SDUI, citing business complexity, release velocity and staffing flexibility as the main drivers. Yandex maintains DivKit, an open-source, Apache-licensed SDUI framework for Android, iOS and web.

Thoughtworks' Technology Radar returned the technique to "Trial" in April 2026, after earlier warnings about "horrendous, overly configurable messes", and recommends it for highly dynamic parts of an app rather than as a blanket replacement. That is the right framing.

How it works: the five building blocks

1. A component registry

The app ships with a fixed library of native components (hero banners, carousels, list rows, buttons), each with a stable type name. The server can only ask for components the app already knows how to draw.

2. A layout schema

The server describes a screen as a tree or list of components, each with a type, properties and optional children. Airbnb's model, for example, separates sections (groups of related components), screens (where and how sections appear, including per-form-factor layouts) and actions (what happens on interaction).

3. Actions and navigation

Taps map to a small vocabulary of named actions the client understands: navigate, open a URL, add to cart, track an event.

4. Data binding

Components carry their own data, or reference values the client resolves locally (the cart count, the user's name). Keep the binding language deliberately small. Once you invent loops and conditionals in JSON, you have built a programming language with no debugger.

5. Versioning, caching and fallbacks

The client sends its app version and supported schema version with each request. The server returns only what that client can render. The last good response is cached so the screen still appears offline, and a bundled default layout covers the very first launch.

A small schema example

Here is a simplified home-feed response:

{
  "schemaVersion": 3,
  "screen": "home",
  "ttlSeconds": 300,
  "sections": [
    {
      "id": "hero-diwali",
      "type": "HeroBanner",
      "props": {
        "title": "Festive offers near you",
        "imageUrl": "https://cdn.example.com/hero/diwali.webp"
      },
      "action": { "type": "navigate", "route": "/offers/festive" }
    },
    {
      "id": "nearby-stores",
      "type": "HorizontalCarousel",
      "props": { "title": "Stores near you" },
      "children": [
        { "id": "s-101", "type": "StoreCard", "props": { "name": "Green Basket", "eta": "20 min" },
          "action": { "type": "navigate", "route": "/store/101" } }
      ]
    },
    {
      "id": "promo-unknown",
      "type": "SpinWheel",
      "fallback": "skip"
    }
  ]
}

Note the last section. An older client that does not recognise SpinWheel should skip it quietly, not crash.

Client-side rendering in TypeScript

The renderer is a lookup table plus a recursive function. This sketch is React Native flavoured, but the same shape works in Flutter, SwiftUI or Jetpack Compose.

type Action =
  | { type: "navigate"; route: string }
  | { type: "openUrl"; url: string }
  | { type: "track"; event: string };

type Section = {
  id: string;
  type: string;
  props?: Record<string, unknown>;
  children?: Section[];
  action?: Action;
  fallback?: "skip" | "placeholder";
};

const registry: Record<string, React.ComponentType<any>> = {
  HeroBanner,
  HorizontalCarousel,
  StoreCard,
};

function handleAction(action?: Action) {
  if (!action) return;
  switch (action.type) {
    case "navigate": return navigation.push(action.route);
    case "openUrl":  return Linking.openURL(action.url);
    case "track":    return analytics.log(action.event);
    default:         return; // unknown action: ignore safely
  }
}

function renderSection(section: Section): React.ReactNode {
  const Component = registry[section.type];
  if (!Component) {
    telemetry.warn("sdui.unknown_component", { type: section.type });
    return section.fallback === "placeholder" ? <EmptySlot key={section.id} /> : null;
  }
  return (
    <Component key={section.id} {...section.props} onPress={() => handleAction(section.action)}>
      {section.children?.map(renderSection)}
    </Component>
  );
}

Validate the response before rendering, log every unknown type, and never let one malformed section take down the screen.

Trade-offs to weigh honestly

Dimension What you gain What it costs
Release speed Layout and content changes go live from the backend New component types still need an app release
Experimentation Server-side A/B tests on layout Experiment logic and analytics move to the backend
Consistency One contract for iOS, Android and web Every platform must implement every component faithfully
Performance Fewer hard-coded screens to maintain Extra network round trip; caching and skeleton states become essential
Debugging Fix many layout bugs without a release Bugs now span server, schema and client; reproduction needs the exact payload
Team structure Backend and product can compose screens You are now maintaining an internal framework, with its own roadmap

An SDUI platform is a product in its own right, and it needs owners.

What the app stores actually allow

This is where loose talk causes real risk, so it is worth being precise.

Apple's App Store Review Guideline 2.5.2 says apps may not "download, install, or execute code which introduces or changes features or functionality of the app". Google Play's Device and Network Abuse policy says an app may not download executable code such as dex, JAR or .so files from a source other than Google Play, and may not update itself outside Google Play's update mechanism.

Well-designed SDUI sits on the right side of these rules because the server sends data, not code. The app already contains every component and action handler; the server only chooses what to show.

The line gets blurry when teams push further: shipping scripts the client evaluates, or toggling on whole features that reviewers never saw. Our practical rules:

  • Only render components that exist in the reviewed binary.
  • Keep actions to a fixed, named vocabulary. No eval, no arbitrary scripting.
  • Do not use SDUI to switch on materially different features or functionality after review.
  • When in doubt, read the current guideline text yourself; both stores revise their policies.

Testing a server-driven app

Because the contract sits between two systems, testing has to cover both ends and the seam between them. This is where quality engineering earns its keep:

  • Schema validation in CI. Every payload the backend can produce is validated against a versioned JSON Schema before deployment.
  • Contract tests. Each client version declares what it supports; the backend is tested against those declarations so it never sends an old app something it cannot draw.
  • Snapshot tests per component. Render each registered component with representative props on each platform and compare against approved images.
  • Golden payloads. Replay real and malformed responses in automated UI tests.
  • Production telemetry. Alert on unknown component types, failed actions and render errors, broken down by app version.

Add preview tooling early so product managers see a layout on a device before it goes live.

A pragmatic adoption path

Do not rebuild the whole app. Start where change is frequent and the risk is low.

  1. Pick one screen. The home feed is usually ideal: it changes often, is largely composed of repeatable sections and is easy to measure.
  2. Define five to ten components. Hero, carousel, list row, card, banner. Resist making them infinitely configurable.
  3. Design the contract first. Agree the schema, action vocabulary and versioning rules before writing renderers.
  4. Build the fallback path. Bundled default layout, cached last-good response and graceful skipping of unknown types.
  5. Put a backend in front of it. A small layout service with validation, caching at the edge and audit logs. Our cloud and DevOps team usually sets this up with staged rollouts so a bad layout can be reverted in minutes.
  6. Measure, then expand. Only take on a second screen once the first has proved its value in release speed or experiment throughput.

We use this incremental approach in hyperlocal commerce, where store listings, offers and categories shift daily. Platforms such as HyperlocalNext are a natural fit because the home screen is effectively a merchandising surface. For broader help choosing an architecture across React Native, Flutter or native, see our product engineering practice.

FAQs

Is Server-Driven UI the same as a web view?

No. A web view renders HTML. SDUI sends a structured description the app renders with native components, keeping native performance and accessibility.

Will Apple or Google reject an app that uses SDUI?

Not for SDUI as such. The rules target downloaded executable code and post-review feature changes. If the app only renders components and actions already in the reviewed binary, you are configuring it, not changing its code.

Does SDUI work with React Native and Flutter?

Yes. The pattern is framework-agnostic, and declarative toolkits such as React Native, Flutter, SwiftUI and Jetpack Compose suit it well.

When should we not use Server-Driven UI?

Avoid it for screens that rarely change, rely on complex gestures or animation, or must work fully offline from first launch. Settings, checkout and onboarding flows are often simpler to keep native.

Planning an SDUI rollout or a new mobile product? Talk to the VCNR team.

Keep reading

Have an idea, a process to fix, or a system to modernize?

Tell us where you are. We’ll reply within one business day with honest next steps, even if they don’t involve us.