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
| Concept | What | Managed In |
|---|---|---|
| SectionDef | Reusable component type schema with settings, blocks, presets | PMS Admin |
| Template | A page bound to app + route (e.g., default + /home) | PMS Admin |
| SectionInstance | An instance of a SectionDef in a template, with setting overrides | Inside Template |
| BlockInstance | A child component within a section | Inside SectionInstance |
| SettingDefinition | A configurable property: TEXT, NUMBER, COLOR, IMAGE, SELECT, etc. | Inside SectionDef |
| BlockSchema | Defines an allowed block type within a section (with its own settings) | Inside SectionDef |
| Preset | Pre-configured section template for quick creation | Inside SectionDef |
| VisibilityRule | Conditional rule: show/hide based on traffic context | On SectionInstance or BlockInstance |
| TemplateVersion | Immutable snapshot created on each publish | Auto-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
| Status | Visible via render API | Editable |
|---|---|---|
| DRAFT | No | Yes |
| PUBLISHED | Yes | Yes (but changes need re-publish) |
| SCHEDULED | Yes (after publishAt) | Yes |
| ARCHIVED | No | No |
Each publish increments the version counter and creates an immutable TemplateVersion snapshot.
Platform Targeting
Templates can target specific platforms:
| Platform | Description |
|---|---|
ALL | Serves all platforms (fallback) |
WEB | Desktop/laptop browsers |
MOBILE | Mobile phones |
TABLET | Tablets |
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:
| Level | Storage | TTL | Eviction |
|---|---|---|---|
| L1 | Caffeine (local per-JVM) | 2 min | On publish/rollback |
| L2 | Redis OM | 10 min | On publish/rollback |
Cache key: {app}:{route}:{platform}
Database Tables
| Table | Purpose |
|---|---|
sdui_section_definitions | Component library (type, settings, blocks, presets) |
sdui_templates | Page definitions (app, route, platform, sections JSON, version) |
sdui_template_versions | Immutable 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
| Module | Endpoint | Context-Aware |
|---|---|---|
| CRS | GET /api/v1/sdui/render | No — returns all sections |
| Website | GET /api/v1/sdui/render | Yes — evaluates visibility rules against traffic context |
| PMS | GET /api/v1/admin/sdui/templates/render | No — 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)