ObjectOS Integration Guide
Complete guide for integrating ObjectUI with ObjectOS platform
ObjectOS Integration Guide
ObjectUI is designed to be the official frontend renderer for the ObjectOS ecosystem. This guide provides comprehensive instructions for integrating ObjectUI components with ObjectOS, ObjectStack, and building enterprise applications.
Overview
ObjectUI serves as the UI Layer in the ObjectOS architecture:
┌─────────────────────────────────────────────────┐
│ ObjectUI (UI Layer) │
│ React Components + Tailwind + Schema Rendering │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ ObjectStack (Runtime Layer) │
│ Kernel + Plugins + ObjectQL + Data Drivers │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ ObjectOS (Platform Layer) │
│ Multi-tenant + RBAC + System Objects + APIs │
└─────────────────────────────────────────────────┘Quick Start Integration
1. Install Dependencies
# Core ObjectUI packages
pnpm add @object-ui/react @object-ui/components @object-ui/fields
# ObjectStack runtime
pnpm add @objectstack/core @objectstack/runtime @objectstack/objectql
# Data adapter
pnpm add @object-ui/data-objectstack
# Optional: Plugins as needed
pnpm add @object-ui/plugin-form @object-ui/plugin-grid2. Set Up ObjectStack Kernel
// src/kernel.ts
import { Kernel } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { AppPlugin } from '@objectstack/plugin-app';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
export async function createKernel() {
const kernel = new Kernel();
// Register essential plugins
kernel.registerPlugin(new ObjectQLPlugin());
kernel.registerPlugin(new AppPlugin());
kernel.registerPlugin(new HonoServerPlugin({
port: 3000,
cors: true
}));
await kernel.start();
return kernel;
}3. Create ObjectStack Configuration
objectstack.config.ts belongs to the ObjectStack server project, not to an
ObjectUI app. It is authored with defineStack() and compiled by the os CLI;
the packages it imports (@objectstack/spec, @objectstack/runtime, plugins)
are dependencies of that project, not of this one. ObjectUI is a pure consumer
of whatever the server publishes over the metadata API, so the authoritative
reference for this file lives on the ObjectStack side:
- Your First Project — the generated
objectstack.config.tswalked through key by key:manifest,plugins,objects - Command Line Interface — how the file is discovered, validated against
ObjectStackDefinitionSchema, and compiled to a deployabledist/objectstack.json - Data Flow Diagrams — how
defineStack()becomes a running application
The rest of this guide is ObjectUI's half of the integration: rendering the objects, views and apps that the server exposes.
4. Set Up Frontend with Console
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { SchemaRenderer } from '@object-ui/react';
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
// Import required plugins
import '@object-ui/components';
import '@object-ui/fields';
import '@object-ui/plugin-form';
import '@object-ui/plugin-grid';
// Initialize ObjectStack adapter
const dataSource = new ObjectStackAdapter({
baseUrl: 'http://localhost:3000/api'
});
function App() {
return (
<div className="min-h-screen bg-background">
<SchemaRenderer
schema={{
type: 'page',
template: 'header-sidebar-main',
header: {
type: 'header-bar',
title: 'My App',
navigation: {
items: [
{ label: 'Dashboard', path: '/', icon: 'home' },
{ label: 'Contacts', path: '/contacts', icon: 'users' }
]
}
},
sidebar: {
type: 'navigation-menu',
items: [
{ label: 'Dashboard', path: '/', icon: 'home' },
{ label: 'Contacts', path: '/contacts', icon: 'users' }
]
},
main: {
type: 'object-view',
objectName: 'contact',
dataSource,
viewTypes: ['grid', 'kanban']
}
}}
/>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);ObjectOS-Specific Features
Multi-Tenancy Support
The adapter's constructor takes no headers option. Per-request headers go
through its fetch hook, which it uses for every call it makes:
// Configure tenant isolation
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
const adapter = new ObjectStackAdapter({
baseUrl: 'http://localhost:3000/api',
fetch: (input, init) => {
const headers = new Headers(init?.headers);
headers.set('X-Tenant-ID', 'tenant-123');
headers.set('X-Workspace-ID', 'workspace-456');
return globalThis.fetch(input, { ...init, headers });
}
});Role-Based Access Control (RBAC)
// Define permissions in object schema
{
objects: {
contact: {
name: 'contact',
label: 'Contact',
permissions: {
create: ['admin', 'sales'],
read: ['admin', 'sales', 'support'],
update: ['admin', 'sales'],
delete: ['admin']
},
fields: {
salary: {
name: 'salary',
label: 'Salary',
type: 'currency',
permissions: {
read: ['admin', 'hr'],
update: ['admin', 'hr']
}
}
}
}
}
}System Objects Integration
ObjectOS provides system objects like sys_user, sys_organization, sys_role, etc. Integrate them in your UI:
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
import type { ObjectViewSchema } from '@object-ui/types';
const objectStackAdapter = new ObjectStackAdapter({
baseUrl: 'http://localhost:3000/api'
});
const sysUserView: ObjectViewSchema = {
type: 'object-view',
objectName: 'sys_user',
dataSource: objectStackAdapter,
defaultViewType: 'grid',
columns: ['username', 'email', 'role', 'status', 'last_login']
};Workflow Integration
// Define workflow-enabled object
{
objects: {
opportunity: {
name: 'opportunity',
label: 'Opportunity',
workflow: {
enabled: true,
states: ['lead', 'qualified', 'proposal', 'negotiation', 'closed_won', 'closed_lost'],
transitions: [
{ from: 'lead', to: 'qualified', label: 'Qualify', role: ['sales'] },
{ from: 'qualified', to: 'proposal', label: 'Create Proposal', role: ['sales'] },
{ from: 'proposal', to: 'negotiation', label: 'Negotiate', role: ['sales', 'manager'] },
{ from: 'negotiation', to: 'closed_won', label: 'Close Won', role: ['manager'] },
{ from: 'negotiation', to: 'closed_lost', label: 'Close Lost', role: ['manager'] }
]
},
fields: {
// ... field definitions
}
}
}
}Data Layer Integration
Using ObjectQL for Queries
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = new ObjectStackAdapter({
baseUrl: 'http://localhost:3000/api'
});
// ObjectQL queries are automatically handled
const schema = {
type: 'object-grid',
objectName: 'contact',
dataSource,
// Grid filter, JSON-rules form: an array of `{ field, operator, value }`
// entries, AND-ed together. `operator` takes the canonical view-filter
// vocabulary (`equals`, `greater_than_or_equal`, ...).
filter: [
{ field: 'status', operator: 'equals', value: 'active' },
{ field: 'created_date', operator: 'greater_than_or_equal', value: '2024-01-01' }
],
// ObjectQL sorting
sort: [
{ field: 'created_date', order: 'desc' }
]
};Custom Data Hooks
@object-ui/data-objectstack ships the adapter, not hooks. Reads and writes
go through useViewData from @object-ui/react, which resolves the adapter
from context and hands back both the rows and the DataSource to write with.
import { SchemaRenderer, useViewData } from '@object-ui/react';
function ContactList() {
const { data, loading, error, dataSource, refresh } = useViewData({
resource: 'contact',
params: {
$filter: { status: 'active' },
$orderby: [{ field: 'name', order: 'asc' }],
$top: 20,
},
});
const handleCreate = async (formData: Record<string, unknown>) => {
await dataSource?.create('contact', formData);
await refresh();
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<SchemaRenderer
schema={{
type: 'object-grid',
rowData: data,
// ... other props
}}
/>
);
}Deployment Strategies
Strategy 1: Monolithic Deployment
Deploy ObjectUI and ObjectStack together in a single Node.js process:
// server.ts
import { createKernel } from './kernel';
import { ConsolePlugin } from './console-plugin';
async function start() {
const kernel = await createKernel();
// Register console UI plugin
kernel.registerPlugin(new ConsolePlugin());
console.log('🚀 Server started at http://localhost:3000');
console.log('📊 Console UI at http://localhost:3000/console');
}
start();Strategy 2: Microservices Deployment
Deploy ObjectUI (frontend) and ObjectStack (backend) separately:
Backend (ObjectStack API):
// backend/server.ts
import { createKernel } from './kernel';
async function start() {
const kernel = await createKernel();
console.log('🚀 API Server started at http://localhost:3000');
}
start();Frontend (ObjectUI):
// frontend/src/config.ts
export const config = {
apiBaseUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
};
// frontend/src/index.tsx
const dataSource = new ObjectStackAdapter({
baseUrl: config.apiBaseUrl
});Strategy 3: Cloud-Native Deployment
Deploy on Kubernetes with separate services:
# k8s/deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: objectstack-api
spec:
selector:
app: objectstack-api
ports:
- port: 3000
---
apiVersion: v1
kind: Service
metadata:
name: objectui-frontend
spec:
selector:
app: objectui-frontend
ports:
- port: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: objectstack-api
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myregistry/objectstack-api:latest
env:
- name: DATABASE_URL
value: postgresql://...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: objectui-frontend
spec:
replicas: 2
template:
spec:
containers:
- name: frontend
image: myregistry/objectui-frontend:latest
env:
- name: API_URL
value: http://objectstack-api:3000Advanced Integration Patterns
Custom Component Registration
// Register custom components with ObjectUI
import { ComponentRegistry } from '@object-ui/core';
ComponentRegistry.register('my-custom-widget', MyCustomWidget, {
namespace: 'custom',
lazy: true
});
// Use in schema
{
type: 'my-custom-widget',
config: {
// custom props
}
}Event Handling & Callbacks
{
type: 'object-grid',
objectName: 'contact',
dataSource,
callbacks: {
onRowClicked: (event) => {
// Navigate to detail page
window.location.href = `/contact/${event.data.id}`;
},
onCellValueChanged: async (event) => {
// Auto-save on edit
await dataSource.update('contact', event.data.id, {
[event.column.field]: event.newValue
});
}
}
}Reacting to Data Changes
⚠️ @object-ui/data-objectstack has no WebSocket transport and no server-push
subscription. What it offers is onMutation: a notification of the writes
this adapter instance performed, which is what a view needs to refresh itself
after its own create/update/delete. It returns its own unsubscribe function.
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
const adapter = new ObjectStackAdapter({
baseUrl: 'http://localhost:3000/api'
});
// Fires for writes this adapter performed. Filter by `resource` for one object.
const unsubscribe = adapter.onMutation((event) => {
if (event.resource !== 'contact') return;
if (event.type === 'create' || event.type === 'update') {
// Refresh the view holding this object's rows
}
});
// Later, when the view unmounts:
unsubscribe();Migration from Other Platforms
From Retool
// Retool table → ObjectUI Grid
{
type: 'object-grid',
objectName: 'users',
dataSource,
editable: true,
rowSelection: 'multiple',
exportConfig: { enabled: true }
}From Appsmith
// Appsmith form → ObjectUI form
{
type: 'object-form',
objectName: 'contact',
dataSource,
mode: 'create',
fieldNames: ['name', 'email', 'phone', 'company'],
onSubmit: async (data) => {
await dataSource.create('contact', data);
}
}From Mendix
// Mendix page → ObjectUI page
{
type: 'page',
template: 'header-sidebar-main',
header: { /* ... */ },
sidebar: { /* ... */ },
main: {
type: 'tabs',
items: [
{ label: 'Overview', content: { /* ... */ } },
{ label: 'Details', content: { /* ... */ } }
]
}
}Performance Optimization
Bundle Optimization
// Lazy load plugins
const plugins = {
grid: () => import('@object-ui/plugin-grid'),
charts: () => import('@object-ui/plugin-charts'),
kanban: () => import('@object-ui/plugin-kanban')
};
// Load on demand
await plugins.grid();Caching Strategy
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
const adapter = new ObjectStackAdapter({
baseUrl: 'http://localhost:3000/api',
cache: {
ttl: 60000, // 1 minute
maxSize: 500 // entries retained
}
});Testing & Quality Assurance
Unit Tests
import { render } from '@testing-library/react';
import { SchemaRenderer } from '@object-ui/react';
test('renders contact grid', () => {
const { getByText } = render(
<SchemaRenderer
schema={{
type: 'object-grid',
objectName: 'contact',
dataSource: mockDataSource
}}
/>
);
expect(getByText('Contact')).toBeInTheDocument();
});Integration Tests
End-to-end tests drive the running app, so they need a browser runner the app itself
does not depend on. Install it in your project first — npm install -D @playwright/test
(then npx playwright install once, for the browsers) — and run these specs with
npx playwright test.
import { test, expect } from '@playwright/test';
test('create contact workflow', async ({ page }) => {
await page.goto('http://localhost:3000/console/contacts');
await page.click('button:has-text("New Contact")');
await page.fill('[name="name"]', 'John Doe');
await page.fill('[name="email"]', 'john@example.com');
await page.click('button:has-text("Save")');
await expect(page.locator('text=John Doe')).toBeVisible();
});Resources
- ObjectStack Documentation
- ObjectUI Components Reference
- ObjectQL Schemas
- Example: ObjectStack Console Starter
- Schema Catalog - every schema rendered in these docs
Support
- GitHub Issues: https://github.com/objectstack-ai/objectui/issues
- Discord Community: https://discord.gg/objectui
- Email: hello@objectui.org