TL;DR: ALPA Mobile has a server-driven UI layer (dynamic feed) where the backend composes pages and the client renders them using a fixed library of Razor components. This doc covers the overall architecture, the current REST contract, and evaluated future paths (real-time push, OTA updates). For field-level contract details see the Dynamic Feed API contract. New to the system? Start at the Dynamic UI Hub — a first-stop guide that walks the whole pipeline through one live screen.
Certain screens in ALPA Mobile are server-driven: instead of the app hard-coding what appears on screen, the backend delivers a page definition (which containers, which items, in what order) and the client materialises it. Think of it as a lightweight CMS — a Blazor-based admin surface lets content editors compose pages, and the device renders exactly what the editor built, because both sides share the same Razor component library.
Not all screens are eligible. Pages with fixed regulatory requirements, complex domain interactions, or platform constraints remain statically defined. Only screens that are explicitly opted in receive a server-driven layout.
PageDto
└── ContainerDto[] ← layout: Carousel, Grid, ButtonGroup, Stack
└── ItemDto[] ← leaf items: HeroCard, CardSmall, ButtonCard, … (renamed from WidgetDto — D39)
The container layer owns all layout authority — background theming, column count, section headers, "View all" links. Individual items never carry layout properties. Standalone items (e.g. a single EmergencyButton) are wrapped in a bare "Stack" container with one item; the wire format is uniform.
Backend API
→ PageDto (JSON)
→ PageFactory → PageViewModel
→ Home.razor iterates PageViewModel.Components
→ ComponentView.razor dispatches container type
→ Carousel.razor / GridContainer.razor / … (Level 1)
→ ComponentView.razor dispatches item type
→ CardHero.razor / CardSmall.razor / … (Level 2)
ComponentView.razor is the same single dispatcher at both levels. PageViewModel sits outside the ComponentViewModel hierarchy — it is the page root, consumed directly by Home.razor, never passed into ComponentView. (Decision: D27)
The dynamic feed is one of three content sources that hydrate the same component library. Every mapping goes through a dedicated pure-mapper factory (never inline field assignment in a page) — see N-TIER-ARCHITECTURE § Content-to-Component Mapping and decisions D58/D60. All three factories are contract-tested (ItemComponentFactoryTests · ContentComponentFactoryTests).
BackgroundToken and PaddingToken in the page response are semantic key names only (e.g. "Surface/Brand", "Spacing/Small"). Resolved values come from a separate theme endpoint queried on login and cached client-side. The theme endpoint delivers MEC-specific override values against Figma-sourced base token keys. The page response never carries raw colour or size values.
Client-side cache is keyed on PageDto.PageId for pages and ItemDto.Id for individual items. No HTTP ETag until the admin UI is built.
Token definitions: design-tokens.html
Status: Working design — endpoint not yet built, contract not finalised. Work item: AB#1821.
The current working model uses a REST endpoint that returns a PageDto JSON payload. The three-level structure (PageDto → ContainerDto[] → ItemDto[]), the container and item discriminator strings, and the factory skip rule for unknown types are captured in the component spec as a baseline for design alignment — the full contract (route, auth, versioning, caching headers) is still being defined with the backend team.
Working design detail: Dynamic Feed API contract
Because these are standard Razor components, the same library is portable to:
This portability is gated on the Presentation Extraction work (Feature AB#2087) that isolates the UI into a clean layer.
Verdict: viable, but REST cannot be fully eliminated.
Microsoft.AspNetCore.SignalR.Client is supported on net10.0-ios and net10.0-android in Blazor Hybrid apps. The push pattern works:
PageDto-equivalent payload when content changesStateHasChanged() — component tree re-renders. No REST poll needed| Constraint | Detail |
|---|---|
| iOS background kill | iOS terminates the SignalR connection when the app backgrounds. No message queue — missed pushes are gone. Connection must be torn down in OnSleep, re-established in OnResume. |
| No queue | If the client misses a push (drop, background), the update is lost. |
| Reconnect ownership | WithAutomaticReconnect() helps but re-subscription to hub methods after reconnect is manual. |
Realistic split: SignalR for live push + a REST seed or hub GetState() call for initial load and iOS reconnect recovery. Not a pure-SignalR replacement.
Verdict: not a viable path for ALPA Mobile's iOS target. The current REST contract is already the correct compliant approach.
| Candidate | Outcome |
|---|---|
Remote BlazorWebView content (point at hosted URL) | Not supported — BlazorWebView always serves from bundled local assets. GitHub issue #24821 open, unresolved. Loading a remote Blazor Server/WASM app in a plain WebView is possible but Apple Guideline 4.7 (updated Nov 2025) now requires mini-apps served via WebView to go through full App Store review. |
| Blazor WASM lazy assembly loading | Not applicable in MAUI — LazyLoadAssemblyLoader relies on the browser JIT. Blazor Hybrid runs on the native .NET runtime; no equivalent API exists. |
| MAUI OTA equivalent (CodePush / Shorebird) | None exist for .NET MAUI. App Center CodePush was React Native only and is retired. |
Assembly.LoadFrom() at runtime | Blocked on iOS (AOT-only, no JIT). Technically possible on Android but Play Store policies restrict downloading new executable logic for core functionality. |
Why the current approach is already correct: the REST-based server-driven feed pushes data and layout decisions (PageDto → ContainerDto → ItemDto), not code. The full component library ships with the app binary. This is the only reliably App Store-compliant path for delivering dynamic UI on iOS — Apple Guideline 3.3.2 permits OTA updates for interpreted content (JSON data, layout) but blocks new compiled/native code outright.
| Document | Location |
|---|---|
| API contract + field-level spec | dynamic-feed-api-contract.html |
| Naming decisions D27–D30 | naming-decisions-record.html |
| Design tokens | design-tokens.html |
| Component spec index | component-specifications/index.html |
| UI Refresh roadmap (AB#1821) | roadmap.html |