ObjectOS is a metadata-driven runtime engine that transforms declarative YAML definitions into fully functional enterprise applications. This document explains the architectural decisions, component interactions, and design principles behind ObjectOS.
ObjectOS is built on the @objectstack/spec protocol, which defines the "DNA" of the ObjectStack ecosystem. The spec provides:
- Zod Schemas: Runtime validation for configuration and data
- TypeScript Interfaces: Compile-time type safety via
z.infer<> - JSON Schemas: VS Code IntelliSense and tooling support
| Namespace | Scope | Key Types |
|---|---|---|
| Data | Business objects, fields, queries | ServiceObject, Field, QueryAST, Hook |
| Kernel | Plugin lifecycle, manifests, context | PluginDefinition, ObjectStackManifest, PluginContextData |
| System | Runtime infrastructure, security | AuditEvent, Job, Event |
| UI | Presentation layer | App, View, Dashboard |
| API | Connectivity contracts | Endpoint, Contract |
The spec defines a standardized plugin lifecycle:
onInstall: First-time setuponEnable: Plugin activationonLoad: Metadata registrationonDisable: Graceful shutdownonUninstall: Cleanup
All ObjectOS plugins must conform to this lifecycle for consistency and predictability.
- Location: https://github.com/objectstack-ai/spec
- Purpose: Defines the protocol and type contracts
- Key Exports:
Data.*- Object schemas, field types, queriesKernel.*- Plugin system, manifests, contextSystem.*- Audit, events, jobsUI.*- App configurations, viewsAPI.*- Endpoint contracts
- Location: https://github.com/objectstack-ai/objectql
- Purpose: Defines the metadata standard and provides core implementations
- Key Packages:
@objectstack/objectql- ObjectQL plugin (data engine, metadata parser, query compiler)@objectstack/driver-memory- In-memory data driver for development and testing
- Location: This repository
- Purpose: Implements the runtime engine and plugin ecosystem
- Key Packages:
@objectstack/runtime- Microkernel with plugin lifecycle management@objectstack/plugin-hono-server- Hono HTTP server plugin@objectos/plugin-better-auth- Authentication plugin@objectos/plugin-audit-log- Audit logging plugin@objectos/kernel- DEPRECATED (use @objectstack/runtime)@objectos/server- DEPRECATED (use @objectos/plugin-server)
"Runtime manages plugins, Plugins implement features, Drivers handle data."
This separation ensures:
- Testability: Each layer can be tested independently
- Flexibility: Add/remove features via plugins without touching core
- Scalability: Plugins can be distributed and loaded dynamically
- Maintainability: Clear boundaries reduce coupling
ObjectQL is a declarative metadata format for describing:
- Business objects (entities)
- Fields and data types
- Relationships (lookup, master-detail)
- Validation rules
- Permission rules
- UI layouts
name: contacts
label: Contact
icon: user
fields:
first_name:
type: text
label: First Name
required: true
last_name:
type: text
label: Last Name
required: true
email:
type: email
label: Email
unique: true
account:
type: lookup
reference_to: accounts
label: Account
permission_set:
allowRead: true
allowCreate: ['sales', 'admin']
allowEdit: ['sales', 'admin']
allowDelete: ['admin']- Human-Readable: Easy for developers to write and AI to generate
- Version-Controllable: Can be tracked in Git
- Language-Agnostic: Can be consumed by any runtime
- Tooling-Friendly: Easy for code generators and validators
The Kernel is responsible for:
- Metadata Loading: Parse and validate
*.object.ymlfiles - Object Registry: Maintain an in-memory registry of all objects
- Query Dispatching: Translate high-level queries to driver calls
- Hook Execution: Run lifecycle hooks (beforeInsert, afterUpdate, etc.)
- Permission Enforcement: Check field-level and record-level permissions
- Relationship Resolution: Handle lookups and related lists
export class ObjectOS {
private registry: Map<string, ObjectConfig>;
private driver: ObjectQLDriver;
private hooks: HookManager;
// Load metadata into registry
async load(config: ObjectConfig): Promise<void>;
// CRUD operations
async find(objectName: string, options: FindOptions): Promise<any[]>;
async insert(objectName: string, data: any): Promise<any>;
async update(objectName: string, id: string, data: any): Promise<any>;
async delete(objectName: string, id: string): Promise<void>;
// Driver management
useDriver(driver: ObjectQLDriver): void;
}Hooks allow custom logic to be executed at specific points:
// Register a hook
kernel.on('beforeInsert', async (context) => {
context.data.created_at = new Date();
context.data.created_by = context.user.id;
});
// Hook types
type HookType =
| 'beforeFind'
| 'afterFind'
| 'beforeInsert'
| 'afterInsert'
| 'beforeUpdate'
| 'afterUpdate'
| 'beforeDelete'
| 'afterDelete';The Kernel never directly instantiates a driver. It receives it via dependency injection:
// ❌ BAD: Hard-coded dependency
class ObjectOS {
constructor() {
this.driver = new PostgresDriver(); // Tight coupling!
}
}
// ✅ GOOD: Injected dependency
const driver = new PostgresDriver({
/* config */
});
const kernel = new ObjectOS();
kernel.useDriver(driver);This allows:
- Unit testing with mock drivers
- Swapping databases at runtime
- Multi-tenant applications with different databases per tenant
All drivers implement the ObjectQLDriver interface:
interface ObjectQLDriver {
// Connection management
connect(): Promise<void>;
disconnect(): Promise<void>;
// Schema management
syncSchema(config: ObjectConfig): Promise<void>;
// CRUD operations
find(objectName: string, options: FindOptions): Promise<any[]>;
findOne(objectName: string, id: string): Promise<any>;
insert(objectName: string, data: any): Promise<any>;
update(objectName: string, id: string, data: any): Promise<any>;
delete(objectName: string, id: string): Promise<void>;
// Query building
buildQuery(objectName: string, filters: FilterGroup): Query;
}- Database Agnostic: Business logic in Kernel works with any database
- Optimized Queries: Each driver can optimize for its database
- Feature Parity: SQL joins vs. MongoDB aggregations are handled internally
| Driver | Package | Databases |
|---|---|---|
| Memory Driver | @objectstack/driver-memory |
In-memory (dev/testing) |
| ObjectQL Plugin | @objectstack/objectql |
SQL, MongoDB via drivers |
The Server layer is a thin HTTP wrapper around the Kernel:
- Request Parsing: Extract parameters from HTTP requests
- Authentication: Validate JWT tokens
- Response Formatting: Convert Kernel output to JSON
- Error Handling: Map exceptions to HTTP status codes
@Controller('api/data')
export class ObjectDataController {
constructor(private kernel: ObjectOS) {}
@Post(':objectName/query')
@UseGuards(AuthGuard)
async query(
@Param('objectName') name: string,
@Body() body: QueryDTO,
@CurrentUser() user: User,
) {
// Controller only handles HTTP translation
return this.kernel.find(name, {
filters: body.filters,
fields: body.fields,
sort: body.sort,
limit: body.limit,
user: user, // For permission checks
});
}
}- Edge-Ready: Works in Node.js, Cloudflare Workers, Vercel, Deno
- Lightweight: Minimal overhead, fast routing
- Middleware: Easy to add CORS, auth, logging, rate limiting
- TypeScript-First: Full type inference for routes and context
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/data/:object/query |
Query records |
| POST | /api/v1/data/:object |
Create record |
| PATCH | /api/v1/data/:object/:id |
Update record |
| DELETE | /api/v1/data/:object/:id |
Delete record |
| GET | /api/v1/meta/:object |
Get object metadata |
| ALL | /api/v1/auth/* |
Authentication (BetterAuth) |
| GET | /api/v1/audit/events |
Audit log events |
| GET | /api/v1/jobs |
Job queue status |
| GET | /api/v1/metrics/prometheus |
Prometheus metrics |
| GET | /api/v1/permissions/sets |
Permission sets |
Note: The UI layer has been moved to a separate project and is no longer part of this monorepo. The UI components are developed independently and can be integrated with ObjectOS through the API layer.
Plugins extend the Kernel with new features:
// Auth Plugin
export function authPlugin(kernel: ObjectOS) {
kernel.on('beforeInsert', async (ctx) => {
ctx.data.owner = ctx.user.id;
});
kernel.on('beforeFind', async (ctx) => {
// Add record-level security filter
ctx.filters.push({ owner: ctx.user.id });
});
}Add new field types:
kernel.registerFieldType('gps_location', {
validate: (value) => {
// Validate GPS coordinates
},
format: (value) => {
// Format for display
},
});Implement ObjectQLDriver for new databases:
class RedisDriver implements ObjectQLDriver {
// Implement interface methods
}Let's trace a request to create a contact:
1. Client sends POST /api/v1/data/contacts
└─> Body: { first_name: "John", last_name: "Doe" }
2. Hono HTTP handler receives request
└─> Extracts user from session (BetterAuth)
└─> Calls kernel broker insert('contacts', data)
3. Kernel processes the request
└─> Loads contact metadata from registry
└─> Runs beforeInsert hooks
└─> Validates required fields
└─> Checks user permissions
└─> Calls driver.insert('contacts', data)
4. Driver executes database operation
└─> PostgresDriver builds SQL query
└─> Executes: INSERT INTO contacts ...
└─> Returns inserted record
5. Kernel post-processes
└─> Runs afterInsert hooks
└─> Returns record to controller
6. Controller returns HTTP response
└─> Status: 201 Created
└─> Body: { id: "...", first_name: "John", ... }
- Implementation: Better-Auth
- Token Type: JWT
- Storage: HTTP-only cookies
- Refresh: Automatic with refresh tokens
Two levels of security:
permission_set:
allowRead: true
allowCreate: ['sales', 'admin']
allowEdit: ['sales', 'admin']
allowDelete: ['admin']// Only show records owned by current user
kernel.on('beforeFind', async (ctx) => {
if (!ctx.user.isAdmin) {
ctx.filters.push({ owner: ctx.user.id });
}
});fields:
salary:
type: currency
visible_to: ['hr', 'admin']- Object definitions are loaded once at startup
- Changes require server restart (hot-reload in dev mode)
- Drivers use database-specific optimizations
- Lazy loading for related records
- Pagination for large datasets
- Database connections are pooled
- Configurable pool size per environment
describe('ObjectOS', () => {
it('should validate required fields', async () => {
const kernel = new ObjectOS();
const mockDriver = createMockDriver();
kernel.useDriver(mockDriver);
await expect(
kernel.insert('contacts', {
/* missing required field */
}),
).rejects.toThrow('first_name is required');
});
});describe('POST /api/data/contacts', () => {
it('should create a contact', async () => {
const response = await request(app)
.post('/api/data/contacts')
.send({ first_name: 'John', last_name: 'Doe' })
.expect(201);
expect(response.body).toHaveProperty('id');
});
});describe('Contact Management', () => {
it('should create and display contact', async () => {
await page.goto('/contacts');
await page.click('button:has-text("New")');
await page.fill('[name="first_name"]', 'John');
await page.fill('[name="last_name"]', 'Doe');
await page.click('button:has-text("Save")');
await expect(page.locator('text=John Doe')).toBeVisible();
});
});┌──────────────────────┐ ┌──────────────────────┐
│ Vite Dev (apps/web) │ │ Fumadocs (apps/site) │
│ :5321 │ │ :3002 │
└──────────┬───────────┘ └──────────┬───────────┘
│ proxy /api/v1 │
▼ │
┌──────────────────────┐ │
│ ObjectStack Hono │◀────────────────┘
│ :5320 │
│ ├── /api/v1/* │
│ ├── /.well-known │
│ └── Kernel + Plugins │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ PostgreSQL / SQLite │
└──────────────────────┘
┌─────────────────┐
│ Load Balancer │
└────────┬────────┘
│
┌────┴────┐
│ │
▼ ▼
┌───────┐ ┌───────┐
│ App 1 │ │ App 2 │
└───┬───┘ └───┬───┘
│ │
└────┬────┘
▼
┌─────────────────┐
│ PostgreSQL │
│ (Primary) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ PostgreSQL │
│ (Replica) │
└─────────────────┘
As the application grows, layers can be split:
- Metadata Service (reads object definitions)
- CRUD Service (handles data operations)
- Auth Service (handles authentication)
Instead of updating records directly:
- Store events (ContactCreated, ContactUpdated)
- Rebuild state from events
- Enables audit trails and time travel
Alternative to REST:
- Single endpoint
- Client specifies fields
- Reduces over-fetching
ObjectOS achieves its goal of being a metadata-driven runtime through:
- Clear Layer Separation: Kernel, Driver, Server have distinct responsibilities
- Protocol Adherence: Strictly implements ObjectQL standard
- Dependency Injection: Enables testing and flexibility
- Extension Points: Hooks and plugins for customization
- Type Safety: Full TypeScript coverage
This architecture allows ObjectOS to generate complete enterprise applications from simple YAML definitions while remaining maintainable, testable, and scalable.