Skip to main content

Server-Driven UI (SDUI)

The server controls what the client renders. Instead of hardcoding layouts in the frontend, the backend returns a JSON structure describing sections, blocks, and settings. The frontend just renders what it receives.

Why SDUI

  • No app releases for layout changes — update a template in PMS, it's live instantly
  • A/B test layouts — different templates for different platforms or traffic contexts
  • Personalization — visibility rules show/hide sections based on partner, UTM, loyalty tier
  • Version control — every publish creates an immutable snapshot, rollback anytime

Data Model

┌─────────────────┐
│ SectionDef │ ← Component library (reusable types)
│ type: "Hero" │
│ settings: [] │ ← Defines available settings + defaults
│ blocks: [] │ ← Defines allowed child block types
│ presets: [] │ ← Pre-configured templates
└─────────────────┘

│ referenced by type

┌─────────────────┐
│ Template │ ← Page definition (bound to app + route)
│ route: /home │
│ platform: ALL │
│ status: DRAFT │
│ version: 0 │
│ sections: [ │
│ ┌───────────────────┐
│ │ SectionInstance │ ← Instance of a SectionDef type
│ │ id: "hero-1" │
│ │ type: "Hero" │
│ │ settings: {...} │ ← Overrides definition defaults
│ │ disabled: false │
│ │ visibilityRules:[]│ ← Conditional rendering
│ │ blocks: [ │
│ │ ┌─────────────────┐
│ │ │ BlockInstance │ ← Child component
│ │ │ id: "btn-1" │
│ │ │ type: "Button" │
│ │ │ settings: {...} │
│ │ │ visibilityRules│
│ │ └─────────────────┘
│ │ ] │
│ └───────────────────┘
│ ] │
└─────────────────┘

│ on publish

┌─────────────────┐
│ TemplateVersion │ ← Immutable snapshot
│ versionNumber: 1│
│ sections: [...]│ ← Frozen copy of sections at publish time
│ publishedBy │
│ publishedAt │
└─────────────────┘

Concepts

ConceptWhatManaged In
SectionDefReusable component type schema with settings, blocks, presetsPMS Admin
TemplateA page bound to app + route (e.g., default + /home)PMS Admin
SectionInstanceAn instance of a SectionDef in a template, with setting overridesInside Template
BlockInstanceA child component within a sectionInside SectionInstance
SettingDefinitionA configurable property: TEXT, NUMBER, COLOR, IMAGE, SELECT, etc.Inside SectionDef
BlockSchemaDefines an allowed block type within a section (with its own settings)Inside SectionDef
PresetPre-configured section template for quick creationInside SectionDef
VisibilityRuleConditional rule: show/hide based on traffic contextOn SectionInstance or BlockInstance
TemplateVersionImmutable snapshot created on each publishAuto-generated

Settings Merge

At render time, definition defaults are merged with instance overrides:

Definition default:  { title: "Default Title", bgColor: "#FFFFFF", subtitle: "Welcome" }
Instance override: { title: "Welcome to ELIVAAS" }
─────────────────────────────────────────────────────────────────────────
Rendered output: { title: "Welcome to ELIVAAS", bgColor: "#FFFFFF", subtitle: "Welcome" }
  • Only overridden keys are replaced
  • Missing keys fall back to definition defaults
  • Extra keys in instance (not in definition) are passed through
  • Blocks have the same merge: BlockSchema defaults + BlockInstance overrides

Template Lifecycle

DRAFT ──publish──► PUBLISHED ──archive──► ARCHIVED

├─ publishAt in future → SCHEDULED (auto-activates)
└─ expireAt set → auto-hides after that time
StatusVisible via render APIEditable
DRAFTNoYes
PUBLISHEDYesYes (but changes need re-publish)
SCHEDULEDYes (after publishAt)Yes
ARCHIVEDNoNo

Each publish increments the version counter and creates an immutable TemplateVersion snapshot.

Platform Targeting

Templates can target specific platforms:

PlatformDescription
ALLServes all platforms (fallback)
WEBDesktop/laptop browsers
MOBILEMobile phones
TABLETTablets

Resolution prefers exact match over ALL:

Request: platform=WEB
1. Template with platform=WEB → preferred
2. Template with platform=ALL → fallback
3. No match → 404

Caching

Render results are cached at two levels:

LevelStorageTTLEviction
L1Caffeine (local per-JVM)2 minOn publish/rollback
L2Redis OM10 minOn publish/rollback

Cache key: {app}:{route}:{platform}

Database Tables

TablePurpose
sdui_section_definitionsComponent library (type, settings, blocks, presets)
sdui_templatesPage definitions (app, route, platform, sections JSON, version)
sdui_template_versionsImmutable publish snapshots (sections JSON frozen at publish time)

All three use soft deletes (deleted_at). Templates have a unique constraint on (app, route) where deleted_at IS NULL.

Render Endpoints

ModuleEndpointContext-Aware
CRSGET /api/v1/sdui/renderNo — returns all sections
WebsiteGET /api/v1/sdui/renderYes — evaluates visibility rules against traffic context
PMSGET /api/v1/admin/sdui/templates/renderNo — admin preview

End-to-End Example

1. Create a section definition

POST /api/v1/admin/sdui/section-definitions
{
"type": "HeroSection",
"name": "Hero",
"settings": [
{ "id": "title", "type": "TEXT", "label": "Title", "defaultValue": "Welcome" },
{ "id": "bgColor", "type": "COLOR", "label": "BG Color", "defaultValue": "#FFFFFF" }
],
"blocks": [
{
"type": "Button",
"name": "CTA Button",
"limit": 2,
"settings": [
{ "id": "label", "type": "TEXT", "label": "Label", "defaultValue": "Click Me" }
]
}
]
}

2. Create a template

POST /api/v1/admin/sdui/templates
{ "name": "Home Page", "route": "/home" }

3. Add sections with overrides and visibility rules

PUT /api/v1/admin/sdui/templates/{id}
{
"sections": [
{
"id": "hero-1",
"type": "HeroSection",
"settings": { "title": "Welcome to ELIVAAS" },
"blocks": [
{ "id": "btn-1", "type": "Button", "settings": { "label": "Explore" } }
]
},
{
"id": "visa-banner",
"type": "BannerSection",
"settings": { "text": "15% off with Visa" },
"visibilityRules": [
{ "field": "partner_id", "operator": "EQUALS", "value": "VISA" }
]
}
]
}

4. Publish

POST /api/v1/admin/sdui/templates/{id}/publish
{ "publishedBy": "admin@elivaas.com" }

5. Render (CRS — all sections)

GET /api/v1/sdui/render?route=/home
→ Both hero-1 and visa-banner returned

6. Render (Website — context-aware)

GET /api/v1/sdui/render?route=/home
→ Visa visitor: hero-1 + visa-banner
→ Non-Visa visitor: hero-1 only (visa-banner hidden by visibility rule)