| title | UI as Data Concept |
|---|---|
| description | The philosophy and architecture of metadata-driven user interfaces |
import { Blocks, Code, Zap, Users, Globe, Shield, Sparkles, Workflow } from 'lucide-react';
Core Thesis: User interfaces are configuration, not code. Forms, dashboards, and reports should be defined in declarative metadata (JSON/YAML), not imperative JavaScript.
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.
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.
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.
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?)
# customer_form.formview.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.
# customer_form.formview.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
# 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: amountWho Can Edit This?
- ✅ Sales Manager (via Dashboard Builder UI)
- ✅ Business Analyst (via YAML editor)
- ❌ No developer required
Result: IT backlog reduced by 60%. Executives build dashboards at 11 PM without filing tickets.
# 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.
┌──────────────────────────────────────────────────────┐
│ 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)
┌─────────────────┐ ┌──────────────────┐
│ Mobile App │ │ Server │
│ │ │ │
│ ┌───────────┐ │ 1. Request Layout │ ┌────────────┐ │
│ │ Generic │ │ ─────────────────→ │ │ Layout │ │
│ │ Renderer │ │ │ │ Resolver │ │
│ │ (Tiny) │ │ 2. Return JSON │ │ │ │
│ │ │ │ ←───────────────── │ │ │ │
│ └───────────┘ │ │ └────────────┘ │
│ ↓ │ │ ↑ │
│ 3. Paint UI │ │ (customer_form │
│ │ │ .formview.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)
GET /api/ui/layouts/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.
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 │
└─────────────────────────────────────────────────────┘
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
}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
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.
Before: 3 frontend teams (Web React, iOS Swift, Android Kotlin)
After: 1 renderer team + Business analysts configure UIs
Savings: $500K/year in engineering costs
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)
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.
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]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 recordsFields 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 checkedfields:
- name: country
type: select
options: [USA, Canada, UK]
- name: state
type: select
dependsOn: country
optionsSource: /api/states?country={country} # ← Fetch states dynamicallyA conformant ObjectUI renderer must:
- Fetch Layout:
GET /api/ui/layouts/{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
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} />;
}| 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 |
- ObjectQL Integration - How ObjectUI leverages ObjectQL schemas
- Renderer Specification - Build your own renderer
- Security Model - Field-level permissions
- FormView API - Complete form configuration reference
- ListView API - Table, kanban, calendar views
- Dashboard API - Widget composition
- Page Builder Guide - Visual form designer
- Dashboard Builder - Drag-and-drop dashboards
- Report Builder - Create custom reports