UI as Data Concept
The philosophy and architecture of metadata-driven user interfaces
Core Thesis: User interfaces are configuration, not code. Forms, dashboards, and reports should be defined in declarative metadata (JSON/YAML), not imperative JavaScript.
The Problem with Traditional UIs
1. Code Coupling Creates Deployment Bottlenecks
Scenario: Marketing needs to add a "promo code" field to the checkout form for Black Friday.
Traditional React Approach:
// CheckoutForm.tsx
function CheckoutForm() {
return (
<form>
<TextField name="email" />
<TextField name="card" />
{/* Add new field here */}
<TextField name="promoCode" /> {/* ← 1 line change */}
<Button>Submit</Button>
</form>
);
}Deployment Pipeline:
- Developer edits
CheckoutForm.tsx(30 minutes) - Run tests (10 minutes)
- Code review (1 day)
- Merge to main (1 day)
- Build production bundle (15 minutes)
- Deploy to staging (30 minutes)
- QA testing (2 days)
- Deploy to production (1 hour)
- iOS: Submit to App Store, wait for approval (3-7 days)
- Android: Upload to Play Store, gradual rollout (2-3 days)
Total Time: 2-14 days for a 1-line change.
Impact: Marketing misses Black Friday. Revenue opportunity lost.
2. Platform Fragmentation Multiplies Work
Same form, 4 implementations:
// Web (React)
<TextField label="Email" value={email} onChange={setEmail} />
// iOS (SwiftUI)
TextField("Email", text: $email)
// Android (Jetpack Compose)
TextField(value = email, onValueChange = { email = it })
// Desktop (Tauri + React)
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />Cost: 4× development time, 4× bug surface, 4× maintenance burden.
Drift Risk: Web form has 5 fields, mobile has 3 (forgot to sync). Users confused.
3. Business Users Are Locked Out
Current State:
- Analyst: "I need a dashboard showing revenue by region"
- IT: "Open a ticket, we'll get to it in Sprint 23 (6 weeks)"
- Analyst: "By then, the board meeting is over"
Why: Dashboards are React components. Analysts don't write code.
Result: IT becomes a bottleneck. Business agility collapses.
4. Validation Logic Gets Duplicated
Email validation written 3 times:
// Frontend (React)
if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
setError('Invalid email');
}
// Backend (Node.js)
if (!validator.isEmail(email)) {
throw new Error('Invalid email');
}
// Database (PostgreSQL)
ALTER TABLE users ADD CONSTRAINT email_format
CHECK (email ~ '^[^\s@]+@[^\s@]+\.[^\s@]+$');Problems:
- 3× maintenance burden (change regex in 3 places)
- Drift risk (frontend and backend regexes differ)
- Source of truth unclear (which validation wins?)
The ObjectUI Solution: UI as Data
1. Define UI in Metadata, Not Code
# customer_edit.view.yml
name: customer_edit
object: customer
layout:
sections:
- label: Contact Information
columns: 2
fields:
- name
- email
- phone
- label: Address
columns: 1
fields:
- street
- city
- postal_code
actions:
- type: standard_save
label: Save Customer
- type: standard_cancel
label: CancelDeployment Pipeline:
- Edit YAML file (2 minutes)
- Commit to Git (30 seconds)
- CI/CD auto-deploys (1 minute)
- All platforms update instantly (web, iOS, Android refresh layout from server)
Total Time: 5 minutes. No app store approvals needed.
2. Write Once, Run Anywhere (For Real This Time)
# customer_form.view.yml (ONE definition)
fields:
- name: email
type: email
required: true
- name: phone
type: tel
required: falseRenderers Translate to Native Widgets:
// Web Renderer (React)
<TextField type="email" required label="Email" />
// iOS Renderer (SwiftUI)
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
// Android Renderer (Jetpack Compose)
OutlinedTextField(
value = email,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
)Result:
- 1 definition → 3 native implementations
- Zero drift: All platforms render the same fields in the same order
- Platform-native UX: iOS users get iOS keyboard, Android users get Material Design
3. Same Metadata Powers Studio Editors and Agent Tools
# sales_dashboard.dashboard.yml
name: sales_overview
label: Sales Overview
layout:
rows:
- widgets:
- type: metric
title: Total Revenue
object: opportunity
aggregate: sum
field: amount
filter: { stage: 'closed_won' }
- type: chart
title: Revenue by Month
chartType: line
object: opportunity
groupBy: close_date
aggregate: sum
field: amountBecause this dashboard is declarative metadata, the same definition can be:
- ✅ Edited by a developer in TypeScript with full type safety
- ✅ Inspected and version-rolled-back in Studio
- ✅ Exposed to an AI agent as a permission-bounded tool ("update the Revenue-by-Month chart filter")
- ✅ Audited — every change is a versioned artifact, not a hand-edited JS string
ObjectStack is not trying to be another drag-and-drop low-code dashboard product. Visual editing is a downstream consequence of having a clean, AI-readable metadata protocol — not the product itself.
4. Single Source of Truth for Validation
# customer.object.yml (ObjectQL)
name: customer
fields:
email:
type: email
required: true
unique: true
phone:
type: tel
format: E.164 # +12125551234
age:
type: number
min: 18
max: 120What Happens Automatically:
- Frontend Validation: Form rejects invalid email before submission
- Backend Validation: API returns 400 if email missing
- Database Constraint: PostgreSQL enforces UNIQUE on email column
- Type Safety: TypeScript types auto-generated (
email: string,age: number)
Result: Write validation once in ObjectQL, enforced everywhere automatically.
The Server-Driven UI Pattern
Traditional Client-Driven UI
┌──────────────────────────────────────────────────────┐
│ Mobile App Bundle │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ CustomerForm.tsx (hardcoded) │ │
│ │ DashboardScreen.tsx (hardcoded) │ │
│ │ ReportView.tsx (hardcoded) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ To change UI → Update code → Rebuild → Resubmit │
└──────────────────────────────────────────────────────┘Problems:
- UI logic burned into app binary
- Change anything → Rebuild everything
- iOS: 3-7 day app review wait
- Users on old app versions see old UI (fragmentation)
ObjectUI Server-Driven UI
┌─────────────────┐ ┌──────────────────┐
│ Mobile App │ │ Server │
│ │ │ │
│ ┌───────────┐ │ 1. Request Layout │ ┌────────────┐ │
│ │ Generic │ │ ─────────────────→ │ │ Layout │ │
│ │ Renderer │ │ │ │ Resolver │ │
│ │ (Tiny) │ │ 2. Return JSON │ │ │ │
│ │ │ │ ←───────────────── │ │ │ │
│ └───────────┘ │ │ └────────────┘ │
│ ↓ │ │ ↑ │
│ 3. Paint UI │ │ (customer_form │
│ │ │ .view.yml) │
└─────────────────┘ └──────────────────┘Benefits:
- App binary is tiny (just renderer logic, no UI definitions)
- Change UI → Update YAML → Server serves new layout instantly
- No app store approval needed
- 100% of users see new UI immediately (no fragmentation)
Layout Request/Response Flow
GET /api/v1/ui/view/customer/form?recordId=12345
Authorization: Bearer <token>Server Response:
{
"type": "form",
"object": "customer",
"mode": "edit",
"title": "Edit Customer - Acme Corp",
"layout": {
"sections": [
{
"label": "Contact Information",
"columns": 2,
"fields": [
{
"name": "name",
"label": "Customer Name",
"type": "text",
"required": true,
"maxLength": 100,
"value": "Acme Corp"
},
{
"name": "email",
"label": "Email",
"type": "email",
"required": true,
"value": "contact@acme.com"
}
]
}
]
},
"actions": [
{
"type": "standard_save",
"label": "Save",
"variant": "primary"
},
{
"type": "standard_cancel",
"label": "Cancel",
"variant": "secondary"
}
],
"permissions": {
"canEdit": true,
"canDelete": false
}
}Renderer Logic (Simplified):
function FormRenderer({ layout }) {
return (
<Form title={layout.title}>
{layout.sections.map(section => (
<Section label={section.label} columns={section.columns}>
{section.fields.map(field => (
<FieldComponent
type={field.type}
label={field.label}
value={field.value}
required={field.required}
/>
))}
</Section>
))}
<ActionBar>
{layout.actions.map(action => (
<Button variant={action.variant}>{action.label}</Button>
))}
</ActionBar>
</Form>
);
}Result: Generic renderer traverses JSON, instantiates components. Zero hardcoded UI logic.
Multi-Layer Resolution Engine
ObjectUI merges 3 layers of configuration to produce the final layout:
┌─────────────────────────────────────────────────────┐
│ Layer 3: User Preferences │
│ - Column widths: { name: 250px, email: 200px } │
│ - Hidden fields: ['internal_notes'] │
│ - Sort preference: { field: 'name', dir: 'asc' } │
└────────────────┬────────────────────────────────────┘
↓ Merge
┌─────────────────────────────────────────────────────┐
│ Layer 2: Admin Configuration │
│ - Custom sections: Added "Billing Info" section │
│ - Field overrides: Made 'phone' required │
│ - Branding: Logo, colors, theme │
└────────────────┬────────────────────────────────────┘
↓ Merge
┌─────────────────────────────────────────────────────┐
│ Layer 1: Base Protocol (ObjectQL Schema) │
│ - Fields from customer.object.yml │
│ - Default validations and types │
│ - Relationships and lookups │
└─────────────────────────────────────────────────────┘Example: Customer Form Resolution
Layer 1: Base Schema (customer.object.yml)
name: customer
fields:
name: { type: text, required: true }
email: { type: email, required: true }
phone: { type: tel, required: false }
status: { type: select, options: [active, inactive] }Layer 2: Admin Customization (Acme Corp tenant)
customizations:
- field: phone
required: true # Override: Make phone required
- field: vip_status
type: boolean
label: VIP Customer # Add custom field
- section:
label: Billing Info
fields: [payment_terms, credit_limit]Layer 3: User Preferences (John Doe)
{
"hiddenFields": ["internal_notes"],
"columnWidths": { "name": 250, "email": 200 },
"theme": "dark_mode"
}Final Merged Layout (Served to John Doe):
{
"sections": [
{
"label": "Contact Information",
"fields": [
{ "name": "name", "required": true, "width": 250 },
{ "name": "email", "required": true, "width": 200 },
{ "name": "phone", "required": true } // ← Admin override
]
},
{
"label": "Billing Info", // ← Admin customization
"fields": [
{ "name": "payment_terms" },
{ "name": "credit_limit" }
]
}
],
"hiddenFields": ["internal_notes"], // ← User preference
"theme": "dark_mode" // ← User preference
}Type Safety and Runtime Validation
ObjectUI definitions are Zod schemas, providing both compile-time and runtime safety:
import { FormViewSchema } from '@objectstack/spec';
import { z } from 'zod';
// Type inference
type FormView = z.infer<typeof FormViewSchema>;
// Runtime validation
const config = FormViewSchema.parse({
name: 'customer_form',
object: 'customer',
layout: {
sections: [
{ label: 'Info', fields: ['name', 'email'] }
]
}
});
// ✅ TypeScript error at compile time
const badConfig: FormView = {
name: 123, // ❌ Type 'number' is not assignable to type 'string'
};
// ✅ Runtime error with clear message
FormViewSchema.parse({
layout: {
sections: [
{ columns: 'two' } // ❌ Expected number, received string
]
}
});
// ZodError: [
// {
// "code": "invalid_type",
// "expected": "number",
// "received": "string",
// "path": ["layout", "sections", 0, "columns"]
// }
// ]Benefits:
- IDE Autocomplete: VSCode suggests all valid properties
- Catch Errors Early: Invalid configs rejected before deployment
- Clear Error Messages: Zod provides actionable feedback
- Self-Documenting: Schema is the documentation
Business Value Delivered
1. 10× Faster UI Iteration
Before ObjectUI:
Product Manager: "Move the email field above the phone field"
Developer: "I'll add it to the sprint"
(2 weeks later)
Developer: "Done. Deployed to production."After ObjectUI:
Product Manager: "Move the email field above the phone field"
(Drags field in Page Builder UI)
Product Manager: "Done. Live in production."
(30 seconds later)Impact: Feature velocity increased 10×. Time-to-market collapsed.
2. 50% Reduction in Frontend Costs
Before: 3 frontend teams (Web React, iOS Swift, Android Kotlin)
After: 1 renderer team + Business analysts configure UIs
Savings: $500K/year in engineering costs
3. Business User Empowerment
Before: IT backlog = 200 tickets, 60% are "Add field to form" or "Create dashboard"
After: IT backlog = 80 tickets, business users self-serve simple changes
Impact: IT focuses on high-value work (integrations, security, performance)
4. Regulatory Compliance Agility
Scenario: GDPR requires adding a "consent" checkbox to all forms with email fields.
Before ObjectUI:
- Audit 300+ React components
- Add checkbox to each form manually
- Test each form
- Deploy to production
- Time: 3 weeks
After ObjectUI:
- Add
gdpr_consentfield to base FormView schema - Checkbox appears on all forms automatically
- Time: 15 minutes
Result: Avoided $500K regulatory fine.
Design Patterns and Best Practices
1. Progressive Disclosure
Show complexity only when needed:
# Simple form for most users
layout:
sections:
- label: Basic Info
fields: [name, email, phone]
# Advanced section (collapsed by default)
layout:
sections:
- label: Basic Info
fields: [name, email, phone]
- label: Advanced Settings
collapsed: true
fields: [api_key, webhook_url, rate_limit]2. Contextual Actions
Actions appear based on state:
actions:
- type: standard_edit
label: Edit
visible: { status: { $ne: 'locked' } } # Hide if locked
- type: workflow_approve
label: Approve
visible: { permissions: { canApprove: true } } # Hide if no permission
- type: standard_delete
label: Delete
confirm: true # Require confirmation
disabled: { has_children: true } # Disable if has child records3. Conditional Field Visibility
Fields appear based on other field values:
fields:
- name: shipping_required
type: boolean
label: Requires Shipping?
- name: shipping_address
type: textarea
label: Shipping Address
visible: { shipping_required: true } # ← Show only if checkbox checked4. Field Dependencies and Cascading
fields:
- name: country
type: select
options: [USA, Canada, UK]
- name: state
type: select
dependsOn: country
optionsSource: /api/states?country={country} # ← Fetch states dynamicallyRenderer Architecture
Renderer Responsibilities
A conformant ObjectUI renderer must:
- Fetch Layout:
GET /api/v1/ui/view/{object}/{viewType} - Parse JSON: Validate against ObjectUI schema
- Instantiate Components: Map JSON types to native widgets
- Handle Events: User clicks → API calls → UI updates
- Manage State: Form values, validation errors, loading states
- Accessibility: Keyboard navigation, screen readers, ARIA labels
Component Registry
Renderers maintain a mapping from JSON types to components:
const componentRegistry = {
text: TextFieldComponent,
email: EmailFieldComponent,
select: SelectComponent,
lookup: LookupComponent,
boolean: CheckboxComponent,
date: DatePickerComponent,
// ... custom components
};
function renderField(field: FieldDefinition) {
const Component = componentRegistry[field.type];
if (!Component) {
throw new Error(`Unknown field type: ${field.type}`);
}
return <Component {...field} />;
}Platform-Specific Renderers
| Renderer | Technology | Status |
|---|---|---|
@objectstack/react-renderer | React + Material-UI | 🚧 In Progress |
@objectstack/vue-renderer | Vue 3 + Vuetify | 📋 Planned |
@objectstack/flutter-renderer | Flutter (iOS/Android) | 📋 Planned |
@objectstack/swift-renderer | SwiftUI (iOS native) | 📋 Planned |
@objectstack/compose-renderer | Jetpack Compose (Android) | 📋 Planned |
@objectstack/tauri-renderer | Tauri + React (Desktop) | 📋 Planned |
What's Next?
Layout DSL
Master page templates, sections, and the grid system
Widget Contract
Standard props and events for all UI components
Action Protocol
Define buttons, triggers, and navigation flows
Related Resources
For Architects
- ObjectQL Integration - How ObjectUI leverages ObjectQL schemas
- Widget Reference - Widget contract for building renderers
- Sharing & Permissions - Field-level and view-level access
For Developers
- View API - Complete FormView & ListView configuration reference (table, kanban, calendar)
- Dashboard API - Widget composition
For Business Users
- Page API - Page & form composition reference
- Object Designer - Model objects and fields
- Flow Builder - Build automation flows