Georgii EmelianovEngineering

Server-driven UI vs generative UI on iOS: the same wire, a different author

If you already run an SDUI backend, you have the transport and none of the validation. The permissive schema that made your payloads easy to author is the part that breaks.

Two identical machined metal plates side by side, one with a wide rectangular opening cut through it and the other with the same opening machined down to a narrow slit.

Show a generative UI demo to a team that already runs server-driven UI and you get the same sentence every time: that is our thing with a model in front of it. It is half right, and the half that is wrong is where all the work lives.

Search server-driven UI vs generative UI and you find two literatures that never touch. The server-driven half is native mobile — Medium tutorials, a Udemy course, a few SwiftUI repositories — with no model anywhere in it. The generative half is React and Flutter, with no compiled app in it. The pieces that do state the difference state it in one sentence and stop.

This one starts where they stop. The sentence is right — a human authors the server-driven payload, a model authors the generative one — but nobody writes down what that costs. It costs three things, and an existing backend helps with none of them. New to the category? Start with what generative UI on iOS actually means.

The short version

Server-driven UI and generative UI ship the same thing over the wire: a serialized description of a screen that a native client renders. The difference is who writes that description, and when. A human writes the server-driven one at deploy time; a model writes the generative one per request, which moves every question about its correctness into production.

Same transport. Both serialize an interface and hand it to a native renderer. Your plumbing carries either.

Different author. Your backend emitted the server-driven payload from code you wrote. The generative payload is composed per user by something with no commit history.

Different problem. Server-driven UI is a deployment problem you solved. Generative UI is a validation problem you have not had yet.

What server-driven UI is

Server-driven UI is an architecture in which the backend sends a description of a screen and the app renders it from components already compiled into the binary. Airbnb, Spotify and Careem all run versions of it, for the same reason: change a screen without an App Store release.

The author is a human and the authoring happens at deploy time. Someone writes the emitter, someone reviews the diff, tests run against fixtures. When the payload is wrong it is wrong for everybody, reproducibly, and a revert fixes it. The component catalog is frozen at release; what the server chooses from it is not.

What generative UI is

Generative UI is an architecture in which a model composes the screen description per request, from a vocabulary of record types the app declares in advance. Same client, same native renderer, same frozen catalog — the model selects and fills records rather than choosing among layouts someone already built.

The authoring happens at request time, once, for one person, with nobody reading it first. No diff, no review, no commit. That is also the point: a per-request author answers questions no designer anticipated, which a frozen set of screens structurally cannot.

What the two actually share

Enough that "we already have this" is a reasonable first reaction rather than a lazy one.

The wire. Both send a serialized interface description to a client that renders it in native views. Neither ships executable code.

The release decoupling. Both change what a screen contains without a binary update.

The versioning problem. Both have to survive a client running an older catalog than the sender assumes, and both solve it by ignoring what the client does not recognise.

The debugging split. Both put half the cause of any visual defect on the far side of a network boundary.

Run server-driven UI today and all four are already solved. You have the transport. What you do not have is anything that assumes the payload might be wrong in a way no deploy caused.


Difference one: correctness moves from deploy time to request time

In a server-driven stack, nearly everything you do about payload correctness happens before the payload exists. Types in the emitter. A snapshot test over a fixture. A staging environment. Code review. By the time bytes leave the server, the interesting question was settled by a compiler and a colleague.

None of that sits anywhere near a generative payload. The model produces it after those checks are over, and none can be moved later. The only place left to inspect it is the request itself, while somebody waits.

So this is not a migration. You are adding a stage the architecture never had: something between the author and the device that reads every line and decides whether it passes.

A server-driven payload is vouched for by the process that produced it. A generative payload has no process behind it — only a gate in front of it.

Difference two: your schema is permissive because a human wrote the payload

This is where an existing server-driven backend stops being a head start and becomes a liability.

Server-driven schemas are loose on purpose: free strings so somebody can write a new headline on a Friday, colour and spacing hints because design tweaks without a release are half the reason the system exists, open-ended nesting so a future layout needs no schema migration.

Every one of those is correct while the author is a colleague, and every one becomes authorial power the moment the author is a model. A free string is a place to put anything. A colour field is permission to author style. Open nesting is permission to invent a layout nobody designed. The schema was never a boundary; it was a convenience extended to a trusted writer.

A typical server-driven component definition looks close to this:

{
  "type": "object",
  "required": ["component"],
  "properties": {
    "component": { "type": "string" },
    "title":     { "type": "string" },
    "textColor": { "type": "string" },
    "padding":   { "type": "number" },
    "icon":      { "type": "string" },
    "children":  { "type": "array", "items": { "$ref": "#" } },
    "action": {
      "type": "object",
      "properties": {
        "label": { "type": "string" },
        "url":   { "type": "string" }
      }
    }
  }
}

Read that as what an author may do and it describes a designer's job. Read it as what a model may do and it lists ways to be embarrassed in front of a paying customer.

The generative version is a different document. Uzori is one implementation — there are others — and its wire format is 23 record types in which every object definition carries additionalProperties: false. Search the file for a colour, font, spacing, padding or corner-radius field and there is nothing to find. Style is absent from the contract rather than discouraged in a prompt; theme lives in the app, never on the wire.

Here is a real record from that file, the one carrying a highlighted statement:

{
  "type": "object",
  "additionalProperties": false,
  "required": ["type", "tone", "body"],
  "properties": {
    "type": { "const": "callout" },
    "tone": { "enum": ["neutral", "information", "success", "warning"] },
    "title": { "type": "string", "minLength": 1, "maxLength": 120 },
    "body": { "type": "string", "minLength": 1, "maxLength": 600 }
  }
}

And the action bar, where the contrast is sharpest:

{
  "type": "object",
  "additionalProperties": false,
  "required": ["type", "actions"],
  "properties": {
    "type": { "const": "handoff_bar" },
    "actions": {
      "type": "array",
      "minItems": 1,
      "maxItems": 2,
      "uniqueItems": true,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["actionRef"],
        "properties": {
          "actionRef": { "type": "string", "minLength": 1, "maxLength": 160 }
        }
      }
    }
  }
}

Three differences carry the argument:

  • additionalProperties: false. A field the contract does not name is not ignored — it makes the record invalid. The permissive version accepts anything and renders what it recognises.
  • tone is a four-value enum, not a colour. The model says something reads as a warning; the app decides what a warning looks like. There is no field a hex value could go in, so there is no prompt to write asking for none.
  • The action is a bare actionRef, capped at two. No label, no URL, no payload. The model can request an action the app declared; it cannot describe one. The server rejects unknown references and the client drops any it was not configured with, so a button survives three layers or never appears.

All three are checkable by reading a file. A contract with nowhere to put a colour is a stronger guarantee than a prompt asking for no colours.

Difference three: a malformed payload stops being a bug and becomes a decision

Server-driven UI never needed the word "reject." A malformed payload was a defect: you found it, fixed the emitter, deployed. No runtime policy, because no runtime question.

Generative UI needs one, per record, in production, with somebody waiting. Three answers exist and they are genuinely different:

Repair. Coerce the record into the nearest legal shape and render it. Tempting, and worst on iOS. A SwiftUI view tree is rebuilt from state rather than patched, so a partial correction does not give you the intended screen with a small error in it. It gives you a different screen — one nobody designed, which looks entirely deliberate to the person holding the phone.

Fail the turn. Honest and expensive: one bad record near the end discards an answer that was mostly right and already visible.

Reject the line. Drop what fails, forward what passes, modify nothing in between.

Repairing a screen does not hand you the answer with a small error in it. It hands you a screen you did not design, shown to a customer as though you had.

Uzori takes the third. Every model-emitted line is checked against JSON Schema, screen identity, navigation match, stream grammar state, grounding and cardinality before anything is forwarded, and the gate forwards the original bytes — so what renders is byte-identical to what passed. Invalid lines are dropped, never patched. One bounded re-ask is permitted, and only when the very first record fails and nothing has reached the device.

The client then re-implements that grammar and its own cardinality limits rather than trusting the gate, and the tighter of the two wins. The rest of this blog works through those stages one at a time.


When server-driven UI is still the right answer

Most of the time, and that is not a hedge.

A model earns its place only when the screen's content depends on a question nobody could have anticipated. Where the decision behind a screen is a product decision — which it usually is — a human should make it. Four cases where adding a model buys nothing and costs review:

  1. Merchandising and promotional layouts. Somebody in marketing decided what belongs there. Encoding that decision is the job; generating it is not.
  2. A/B tests. The value is that a human chose two variants and can attribute the result. A per-request author destroys the experiment.
  3. Remote configuration of an existing flow. Reordering onboarding, hiding a field, moving a threshold. Configuration, not composition.
  4. Anything where the same input must produce the same screen. Regulatory disclosures, pricing breakdowns, anything likely to be screenshotted into a complaint.

Server-driven UI also carries known costs — no offline render, a round trip before anything appears, a defect whose cause sits on either side of the boundary. Generative UI inherits every one and fixes none.

Which one you should pick

Pick server-driven UI when a human already knows what the screen should say. That covers most screens in most apps, and the fact that a model could compose them is not an argument that it should.

Pick generative UI when the question arrives with the request. "Why is my bill higher this month" has no screen you could have built in advance: the answer is a comparison, three line items and a date that differ for everyone who asks.

Expect to run both. The server-driven backend keeps doing its job; the generative surface is a different surface with a tighter contract and a gate in front of it.

Two disqualifiers. Generated screens fit questions whose answers are read rather than written — a bill, a delayed flight, a denied claim — and they fight commerce, because you cannot rent a dress read-only. In Uzori's tool layer mutating verbs are disabled by default, and an operation reaches the model only if a human both allowlists it and classifies it as read. Second, this is iOS. There is no Android implementation, which matters if your rollout has to be both.

Frequently asked questions

Is generative UI just server-driven UI with an LLM?

No — it shares the transport and almost nothing else. Both send a serialized screen description to a native client. But a human authors the server-driven one at deploy time, where a compiler and a code review inspect it first. A model authors the generative one per request, after those checks are done.

Can I reuse my existing server-driven UI backend for generative UI?

The transport, yes. The schema, no. Server-driven schemas are permissive because a trusted human wrote the payload — free strings, style hints, open nesting — and those permissions become authorial power once a model holds the pen. Keep the delivery layer, close the contract, add the validation stage your stack has never needed.

When should I use server-driven UI instead?

Whenever a human already knows what the screen should say. Merchandising, promotional layouts, A/B tests and remote configuration of an existing flow are product decisions, and a per-request author only adds review surface. Use it too wherever the same input must produce the same screen — regulatory disclosures, pricing breakdowns.

What happens if the model asks for a component the app does not have?

The record is invalid and gets dropped before reaching the device. This is why the record vocabulary stays small and general rather than long and specific. On iOS a new component reaches people through App Store review, then gradually as installs update, so "just add a component" is measured in weeks.

What to do with the backend you already have

Keep it. The server-driven stack is not what generative UI replaces; it is what generative UI sits beside, and the transport work you already did is work you do not repeat. What you add is a contract tight enough to survive an author you cannot review, and a stage that reads every line against it before a device sees one.

To find out whether that is worth doing, take the question your customers ask most and check whether the honest answer fits in a sentence. If it is a comparison, a set of figures or a sequence of steps, a paragraph was never going to carry it. Send that question to hello@uzori.ai and we will show you it as a screen.

← All posts