06

SDUI in Alphazed

A completely different product — a kids' learning app — reaches for the same idea, and pushes it further: three server-driven surfaces running at once, from onboarding flows to learning games to home-screen tabs.

A second case study, the same idea

So far the case study has been a medical imaging platform. Alphazed is the opposite kind of product — a mobile learning app for children — but it lands on the same conclusion: the UI is data. Its backend is a Python/Flask service; its client is a Flutter app that owns only how things look. The backend never sends views; it sends typed JSON, and the client's registries render it.

What makes Alphazed a richer example than a single page-of-sections is that it drives three server-driven surfaces at the same time, each authored differently and served by its own endpoint. A surface here just means one area of the app the server describes as data.

Three server-driven surfaces at once

The three surfaces differ in what they drive and how they are authored, but they share the same contract: the server ships typed JSON, the client renders it. One is authored as static JSON files in the repo, one as rows in a database, one as objects built in code. All three arrive as JSON the client has never seen a screen for.

SurfaceDrivesAuthored asServed by
Workflow layerAuth / onboarding / paywall flows (a state machine of screens)Static JSON files in the repo/app/vN/mobile-app/init
Content layerLearning games and questions (bytes made of bits)Rows in MySQL/user/v2/content-byte/start
Screen / section layerMain-app tabs (play / progress / subject) as widget listsCode-built section objects/user/v3/screen/:name

Some vocabulary this day uses, glossed once: a workflow is a flow of screens described as a state machine; a step is one node in that machine; a content byte is one learning game (a container); a content bit is one activity inside it (a question, an animation, a mini-game); media is the text, image, audio, or video a bit shows. The rest of the day walks the three surfaces in turn.

Surface 1 — the workflow layer

The first surface describes flows: sign-in, sign-up, onboarding, paywall. Each flow is a static JSON file in the repo, validated against a schema (workflow.schema.json, contract version 2), and shipped to the client by the bootstrap endpoint /app/vN/mobile-app/init. The client runs it as a state machine — no per-flow code on either side.

The top level names an id, the start_step_id to begin at, a presentation block that says who owns the navigation stack and where the flow exits to, and the list of steps.

{
  "id": "post_sign_up_amal_v6",
  "contract_version": 2,
  "start_step_id": "avatar_customization",
  "presentation": { "stack_owner": "workflow", "exit_route": "main_app" },
  "steps": [ /* Step objects */ ]
}

The id carries a version suffix (post_sign_up_amal_v6) so analytics can compare funnels by exact flow version. Everything else about running the flow is in the steps.

A step, and the four transitions

A step is one node in the state machine. It names an id, a screen (a string key the client resolves in its screen registry to know what to build), a presentation mode for how it enters, a back policy, and a transitions map that says where each outcome leads.

{
  "id": "positioning_mirror",
  "type": "screen",
  "screen": "multi_section",
  "presentation": "push",
  "back": "previous_step",
  "transitions": { "complete": "lo_promise", "skip": "lo_promise" }
}

The screen value (multi_section) is just a key — the backend ships the key plus a free-form config, and the client's registry knows how to build it. presentation is one of push, replace, root, or modal; back is previous_step, exit_workflow, or disabled. The important part is transitions: it is the state machine's edges. Its keys are exactly fourcomplete, skip, fail, error — and nothing else is allowed. Each key's value is either a real step id or the sentinel string $END, which terminates the flow (and hands off per the top-level exit_route). A null target is forbidden — every declared outcome must go somewhere. (Note: a sibling frontend template once allowed null here; this backend contract does not — treat them as two versions of the same idea, and this one is stricter.)

Because the whole flow is this graph of steps and edges, the backend can validate it before shipping: it checks that start_step_id resolves, that every transition target is a real step or $END, and that the flow can actually reach an end. If a flow fails that check it is simply left out of the init response, and the client falls back to a legacy flow — a broken flow can never strand a user mid-onboarding.

multi_section — a screen is a keyed dict of sections

When a step's screen is multi_section, the step's config.sections describes what to render — and it is a dict keyed by section name, not a bare array. Each value is a typed section (question_screen, paywall_v2, image_carousel, video_carousel, reviews_carousel, and so on) with its own fields. Buttons carry an action_type (like advance_workflow) and an identifier used for analytics. User-visible text is a locale dict — { "en": …, "ar": … } — that the backend resolves per user before it ships.

{
  "id": "positioning_mirror",
  "screen": "multi_section",
  "config": {
    "sections": {
      "hero": {
        "type": "image_carousel",
        "images": ["intro_1", "intro_2"]
      },
      "cta": {
        "type": "paywall_v2",
        "title": { "en": "Unlock every subject", "ar": "افتح كل المواد" },
        "action_type": "advance_workflow",
        "identifier": "post_signup_paywall"
      }
    }
  }
}

So the workflow layer nests: a flow is a list of steps, a multi_section step is a keyed set of sections, and each section is a typed widget the client's section registry knows how to draw. The backend passes config to the builder verbatim; it never assembles the screen itself.

Surface 2 — the content layer: bytes of bits

The second surface is the actual learning content, and it is authored not as files but as rows in a MySQL database, served by /user/v2/content-byte/start. A content byte is one learning game — a container with an identity like a skill, story, or dialogue. It composes an ordered list of content bits, and each bit is one activity: a narration, a question, a matching game, a drag-and-drop, a coloring canvas. The backend defines around sixty bit types; the client renders each by type.

{
  "type": "skill",
  "content_bits": [
    { "id": 1, "type": "fragment",    "main_text": "Let's count!" },
    { "id": 2, "type": "select-text", "main_text": "Which is 3?" },
    { "id": 3, "type": "drag-and-drop", "properties": { "grid": 3 } }
  ],
  "coins_award": 10
}

Two things make this SDUI rather than a fixed content format. First, the type on every byte and bit is a wire string the client looks up in a registry — a new game type is a new row plus a registered widget, not a new screen. Second, a bit's behavior travels with it as data: a properties blob holds per-widget config, a correct_answers_options field holds the answer key, and answers can even point to the next bit to play — so the flow within a byte is data too, not code.

The composition chain

Content does not float free; it hangs off a taxonomy so the app can show the right byte to the right child. The chain runs from the age group down to the media on screen, with one junction worth naming: sag is the subjects_age_groups table, the join that says which subjects are available to which age group (and on which platform and language, and how the subject list is laid out). A subject groups chapters (or, in the newer model, concepts), a chapter holds content bytes, a byte holds ordered content bits, and each bit resolves to media.

Read left to right, this is how /user/v2/content-byte/start knows what to return: given a child's age group, the sag junction yields their subjects, a subject's chapter points at a byte, the byte's ordered bits are dumped to the client, and each bit pulls its media. Every arrow is a database relationship, so re-targeting content is a data change, not a code change.

Surface 3 — the screen/section layer

The third surface is the app's own home once a child is past onboarding — the main tabs like play, progress, and subject. These are served by /user/v3/screen/:name, and unlike the other two they are authored in code: the backend builds a list of typed section objects for each tab. It is the same section idea as the SpatialX canvas from Day 5 — a screen is an ordered list of typed sections the client renders — applied to a home screen instead of a page. A companion route, /user/v3/screen/:name/section/:section, lazy-loads a single section on its own, so a heavy tab can stream its sections in rather than block on all of them.

The point of showing all three together is the range: the same "server ships typed JSON, client renders by registry" contract stretches from static onboarding files, through database-authored games, to code-built home tabs. One idea, three authoring styles, three endpoints — and a client that owns only how each piece looks.

Key takeaways

  • Alphazed is a different product (a kids' learning app on Flask + Flutter) reaching the same SDUI conclusion, and it runs three server-driven surfaces at once: workflow, content, and screen/section — each authored differently and served by its own endpoint.
  • The workflow layer is a state machine: workflow.schema.json v2 gives { id, start_step_id, presentation, steps[] }, and a step is { id, screen, presentation, back, transitions }.
  • transitions keys are exactly complete, skip, fail, and error; each maps to a real step id or the sentinel $END, and null targets are forbidden — so the flow is a fully-defined graph the backend can validate before shipping.
  • A broken workflow is omitted from /app/vN/mobile-app/init entirely, so the client falls back to a legacy flow rather than stranding a user.
  • A multi_section screen's config.sections is a dict keyed by section name; each value is a typed section (question_screen, paywall_v2, image_carousel, …) with locale-dict text resolved server-side.
  • The content layer authors learning games as MySQL rows — a content_byte composes an ordered list of content_bits (~60 types), each carrying its behavior as data (properties, correct_answers_options, next-bit branching).
  • The composition chain is age_group ←(sag)→ subject → chapter/concept → content_byte → [content_bits] → media; every arrow is a database relationship, so re-targeting content is a data change.
  • The screen/section layer serves code-built section lists at /user/v3/screen/:name, with per-section lazy loading — the same section idea as the canvas, applied to home tabs.

Checklist

  • [ ] I can name Alphazed's three server-driven surfaces and the endpoint that serves each.
  • [ ] I can read a workflow JSON and point to id, start_step_id, presentation, and steps.
  • [ ] I can read a step and explain screen, presentation, back, and transitions.
  • [ ] I can state the exactly-four transition events and explain what $END does and why null is forbidden.
  • [ ] I can explain why a workflow that fails validation is dropped from init, and what the client does instead.
  • [ ] I can describe a multi_section screen as a keyed dict of typed sections with server-resolved locale text.
  • [ ] I can explain the byte → bits → media relationship and why a new bit type is a row plus a registered widget, not a new screen.
  • [ ] I can trace the composition chain from age_group through the sag junction down to media.