Architecture Overview
Deep dive into the ObjectUI 3-layer architecture, data flow, plugin system, state management, and action system.
Architecture Overview
ObjectUI is a Server-Driven UI (SDUI) engine that transforms JSON schemas into fully interactive React interfaces built on Shadcn/Tailwind. This document covers the internal architecture, data flow, and extension points.
The 3-Layer Architecture
ObjectUI enforces a strict separation across three layers. Each layer has clear constraints on what it may import and what it may contain.
┌─────────────────────────────────────────────────────────────────────┐
│ Layer 1: @objectstack/spec v3.0.0 (The Protocol) │
│ Pure TypeScript type definitions — 12 export modules │
│ ❌ No runtime code. No React. No dependencies. │
└──────────────────────────────┬──────────────────────────────────────┘
│ imports (never redefines)
┌──────────────────────────────▼──────────────────────────────────────┐
│ Layer 2: @object-ui/types (The Bridge) │
│ Re-exports spec types + ObjectUI-specific schemas │
│ ❌ No runtime code. Zero runtime dependencies. │
└──────────────────────────────┬──────────────────────────────────────┘
│ consumed by
┌──────────────────────────────▼──────────────────────────────────────┐
│ Layer 3: Implementations (The Runtime) │
│ core, react, components (91+), fields (35+), plugins, etc. │
└─────────────────────────────────────────────────────────────────────┘Layer 1 — @objectstack/spec (The Protocol)
The upstream JSON specification for all ObjectStack products. ObjectUI imports these types but never redefines them. Located externally; consumed as @objectstack/spec ^3.0.0.
Layer 2 — @object-ui/types (The Bridge)
Lives in packages/types/. Re-exports spec types and adds ObjectUI-specific schemas (component schemas, widget props, field props). Has zero runtime dependencies — only type-level imports of @objectstack/spec and zod for validation schemas. Marked sideEffects: false.
Layer 3 — Implementations (The Runtime)
All packages that ship runnable code: core, react, components, fields, layout, i18n, auth, permissions, tenant, and every plugin-* package.
Package Dependency Graph
@objectstack/spec
│
▼
@object-ui/types ◄─────────────────────────────────────┐
│ │
▼ │
@object-ui/core │
│ (registry, evaluator, actions, validation) │
▼ │
@object-ui/react ──────► @object-ui/i18n │
│ (SchemaRenderer, hooks, contexts) │
├──────────────────────────────────────┐ │
▼ ▼ │
@object-ui/components @object-ui/fields │
│ (91+ Shadcn atoms) (35+ inputs) │
▼ ▼ │
@object-ui/layout @object-ui/plugin-* │
(AppShell, Header, (grid, kanban, charts, │
Sidebar, routing) dashboard, form, etc.) │
│ │
└─────────────────┘
(all packages import types)Strict rules:
| Package | May Import | Must NOT Import |
|---|---|---|
types | @objectstack/spec | Any runtime package |
core | types, lodash, zod | React, any UI library |
react | core, types, i18n | Plugin packages directly |
components | types (for props) | core, react |
fields | types (for FieldWidgetProps) | core business logic |
plugin-* | Any package | Other plugins |
Data Flow: Schema → Screen
The rendering pipeline transforms a JSON schema into a live React component tree:
JSON Schema (from API or static file)
│
▼
┌─────────────┐ ┌──────────────────┐
│ SchemaRenderer │───►│ ExpressionEvaluator │
│ (packages/ │ │ Resolves ${...} │
│ react/src/) │ │ expressions │
└──────┬────────┘ └──────────────────┘
│
▼
┌─────────────────┐
│ ComponentRegistry │ ← registry.resolve(schema.type)
│ (packages/core/ │
│ src/registry/) │
└──────┬──────────┘
│
▼
┌──────────────────┐
│ React Component │ ← Wrapped in ErrorBoundary
│ (Button, Grid, │ with ARIA attributes
│ Kanban, etc.) │
└──────────────────┘- SchemaRenderer (
packages/react/src/SchemaRenderer.tsx) receives a JSON schema object. - It evaluates dynamic expressions via
ExpressionEvaluator(packages/core/src/evaluator/). - It looks up the component type in the
ComponentRegistry(packages/core/src/registry/Registry.ts). - The matched component renders with schema props, wrapped in a per-component
ErrorBoundarywith ARIA accessibility attributes (aria-label,aria-describedby,role).
The Plugin System
Plugins are self-contained packages that register heavy or complex views (grids, kanbans, charts). They follow a consistent pattern defined in packages/core/src/registry/PluginSystem.ts.
Plugin Lifecycle
import 'plugin-kanban'
│
▼
PluginSystem.load(plugin)
│ ├─ Check dependencies
│ ├─ Prevent duplicate loading
│ └─ Call plugin.register(scope)
▼
PluginScope.registerComponent(type, Component, meta)
│ └─ Auto-prefixes with plugin namespace
▼
ComponentRegistry.register('kanban-ui', KanbanRenderer, {
namespace: 'plugin-kanban',
category: 'plugin'
})Lazy Loading
Plugins use React.lazy() wrapped by LazyPluginLoader (packages/react/src/LazyPluginLoader.tsx):
- Retry logic: 2 retries with 1-second delay by default
- Custom fallbacks: Loading skeleton + error boundary
- Tree-shaking: Plugins are code-split from the initial bundle
Plugin Registration Pattern
Every plugin follows the same structure (see packages/plugin-kanban/, packages/plugin-grid/):
// 1. Lazy-load the heavy implementation
const LazyKanban = React.lazy(() => import('./KanbanImpl'));
// 2. Define a thin wrapper with Suspense
const KanbanRenderer: React.FC<Props> = ({ schema }) => (
<Suspense fallback={<Skeleton />}>
<LazyKanban schema={schema} />
</Suspense>
);
// 3. Register in the global registry
ComponentRegistry.register('kanban-ui', KanbanRenderer, {
namespace: 'plugin-kanban',
label: 'Kanban Board',
category: 'plugin',
inputs: [/* schema config */]
});Scaffold a new plugin with npx @object-ui/create-plugin.
State Management
ObjectUI uses React Context for all state management, with contexts scoped to specific concerns:
Core Contexts (packages/react/src/context/)
| Context | Purpose |
|---|---|
SchemaRendererContext | Data scope + debug mode for the rendering tree |
ActionContext | Action runner instance for handling user interactions |
ThemeContext | Theme tokens and dark/light mode |
NotificationContext | Toast and notification system |
DndContext | Drag-and-drop state for sortable views |
Domain Contexts (separate packages)
| Context | Package | Purpose |
|---|---|---|
AuthContext | packages/auth/ | Authentication state |
PermissionContext | packages/permissions/ | RBAC/ABAC permissions |
TenantContext | packages/tenant/ | Multi-tenant isolation |
I18nProvider | packages/i18n/ | Locale and translations |
Contexts are composed at the application root and consumed by plugins and components via hooks (e.g., useActionRunner, useExpression, useViewData).
Expression Evaluation
The expression engine lives in packages/core/src/evaluator/ and powers dynamic schemas:
Components
ExpressionEvaluator.ts— Main engine: parses and evaluates${...}template expressions.ExpressionContext.ts— Variable scope: providesdata,user,paramsto expressions.ExpressionCache.ts— Caches parsed expressions for repeated evaluations.FormulaFunctions.ts— Built-in functions available inside expressions.
Expression Types
{
"visible": "${data.role === 'admin'}",
"label": "Hello, ${data.user.name}!",
"disabled": "${data.status !== 'draft'}",
"className": "${data.priority === 'high' ? 'text-red-500' : 'text-gray-500'}"
}Expressions support:
- String interpolation:
"Welcome, ${data.name}" - Conditionals:
"${data.age > 18}" - Ternary operators:
"${data.active ? 'Yes' : 'No'}" - Variable references:
data.*,user.*,params.*
Action System
The action system (packages/core/src/actions/) handles user interactions defined in schemas.
ActionRunner (ActionRunner.ts)
Executes action schemas and returns directives:
User Click → Schema Action
│
▼
ActionRunner.execute(action, context)
│ ├─ Check confirmation dialog
│ ├─ Evaluate conditions
│ └─ Execute by action type
▼
ActionResult
├─ reload: boolean
├─ redirect: string
├─ modal: SchemaObject
└─ toast: { message, type }Supported Action Types
| Type | Description |
|---|---|
script | Execute inline JavaScript |
url | Navigate to a URL |
api | Make an API request (AJAX) |
modal | Open a modal with a nested schema |
flow | Execute a multi-step action sequence |
TransactionManager (TransactionManager.ts)
Wraps multi-step actions in transactions for consistency, supporting rollback on failure.
Key Directories Reference
packages/
├── types/src/ # Layer 2 — Pure type definitions
├── core/src/
│ ├── evaluator/ # Expression engine
│ ├── registry/ # Component & plugin registries
│ ├── actions/ # ActionRunner, TransactionManager
│ ├── validation/ # Schema validation engine
│ ├── adapters/ # Data source adapters (API, Value)
│ ├── data-scope/ # DataScopeManager
│ ├── query/ # Query AST (filtering/sorting)
│ ├── theme/ # ThemeEngine
│ └── builder/ # Schema builder utilities
├── react/src/
│ ├── SchemaRenderer.tsx
│ ├── LazyPluginLoader.tsx
│ ├── context/ # All React contexts
│ └── hooks/ # 20+ hooks (useExpression, useActionRunner, etc.)
├── components/ # 91+ Shadcn UI atoms
├── fields/ # 35+ field renderers
├── layout/ # AppShell, Header, Sidebar
├── i18n/ # Internationalization
├── auth/ # AuthContext + providers
├── permissions/ # RBAC/ABAC
├── tenant/ # Multi-tenancy
└── plugin-*/ # 20 plugin packages
├── plugin-grid/
├── plugin-kanban/
├── plugin-charts/
├── plugin-dashboard/
├── plugin-form/
└── ... (15 more)Further Reading
- Schema Overview — JSON schema structure
- Schema Rendering — How schemas become UI
- Component Registry — Registering components
- Plugin Development — Building plugins
- Expressions — Expression syntax reference
- Theming — Theme configuration