ObjectUIObjectUI

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-grid

2. 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.ts walked through key by key: manifest, plugins, objects
  • Command Line Interface — how the file is discovered, validated against ObjectStackDefinitionSchema, and compiled to a deployable dist/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:3000

Advanced 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

Support

On this page