| title | MicroKernel Architecture |
|---|---|
| description | Understanding ObjectStack's micro-kernel plugin architecture for building extensible applications |
ObjectStack uses a micro-kernel architecture that separates core functionality from business logic. Like the Linux kernel, the ObjectKernel provides minimal essential services while all features are loaded as plugins.
The MicroKernel architecture enables:
- ✅ Pluggable ObjectQL instances (bring your own query engine)
- ✅ Service registry for dependency injection (DI)
- ✅ Standardized plugin lifecycle (init/start/destroy)
- ✅ Event-driven communication between plugins
- ✅ Easy testing with mockable services
┌─────────────────────────────────────────────────────────┐
│ ObjectKernel (Core) │
│ • Plugin Lifecycle Manager │
│ • Service Registry (DI Container) │
│ • Event Bus (Hook System) │
│ • Dependency Resolver │
└──────────────┬──────────────────────────────────────────┘
│
┌───────┴────────┬────────────┬──────────┐
│ │ │ │
┌────▼─────┐ ┌─────▼─────┐ ┌──▼───┐ ┌───▼────┐
│ ObjectQL │ │ Driver │ │ Hono │ │ Custom │
│ Plugin │ │ Plugin │ │Server│ │ Plugin │
└──────────┘ └───────────┘ └──────┘ └────────┘
The kernel manages plugin lifecycle and provides core services.
Location: packages/core/src/kernel.ts
Key Features:
- Plugin Lifecycle Management: Three-phase lifecycle (init → start → destroy)
- Dependency Resolution: Topological sort ensures correct initialization order
- Service Registry: Dependency injection container for inter-plugin communication
- Event Bus: Hook system for loose coupling between plugins
- Universal Logger: Pino-based logging for server and browser environments
API:
import { ObjectKernel } from '@objectstack/core';
import { DriverPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
const kernel = new ObjectKernel();
// Register plugins
kernel.use(new ObjectQLPlugin())
.use(new DriverPlugin(driver, 'memory'));
// Start the kernel
await kernel.bootstrap();
// Access services
const ql = kernel.context.getService('objectql'); // Or via getKernel() if exposed
// Shutdown
// await kernel.shutdown(); // (Future)All plugins must implement the Plugin interface.
Location: packages/core/src/types.ts
Definition:
interface Plugin {
name: string; // Reverse domain notation (e.g., 'com.objectstack.driver.postgres')
version?: string; // Semantic version (e.g., '1.0.0')
dependencies?: string[]; // Plugin names this depends on
init(ctx: PluginContext): Promise<void> | void; // Register services
start?(ctx: PluginContext): Promise<void> | void; // Execute business logic
destroy?(): Promise<void> | void; // Clean up resources
}Plugin Naming Convention:
- Use reverse domain notation for global uniqueness
- Format:
{domain}.{category}.{name} - Examples:
com.objectstack.engine.objectqlcom.objectstack.driver.postgrescom.acme.crm.customer-management
┌──────┐
│ idle │
└──┬───┘
│ kernel.use(plugin)
▼
┌──────┐
│ init │ ← Register services, subscribe to events
└──┬───┘
│ kernel.bootstrap()
▼
┌───────┐
│ start │ ← Connect to databases, start servers
└──┬────┘
│
▼
┌─────────┐
│ running │
└──┬──────┘
│ kernel.shutdown()
▼
┌─────────┐
│ destroy │ ← Clean up resources
└─────────┘
The context provides access to kernel services and hooks.
API:
interface PluginContext {
// Service Registry
registerService(name: string, service: any): void;
getService<T>(name: string): T;
getServices(): Map<string, any>;
// Event System
hook(name: string, handler: Function): void;
trigger(name: string, ...args: any[]): Promise<void>;
// Logger (Pino-based)
logger: Logger;
// Kernel Access
getKernel(): ObjectKernel;
}Logger Methods:
logger.debug(message, metadata?) // Development/troubleshooting
logger.info(message, metadata?) // General information
logger.warn(message, metadata?) // Warnings
logger.error(message, metadata?) // Errors
logger.fatal(message, metadata?) // Fatal errorsName: com.objectstack.engine.objectql
Location: packages/objectql/src/plugin.ts
Registers the ObjectQL query engine as a service.
import { ObjectKernel } from '@objectstack/core';
import { ObjectQLPlugin } from '@objectstack/objectql';
const kernel = new ObjectKernel();
// Default ObjectQL instance
kernel.use(new ObjectQLPlugin());
// Or bring your own instance
import { ObjectQL } from '@objectstack/objectql';
const customQL = new ObjectQL({ /* config */ });
kernel.use(new ObjectQLPlugin(customQL));Services Registered:
objectql- ObjectQL engine instanceprotocol- Protocol implementation shim
Lifecycle:
- init: Registers ObjectQL as a service
- start: Discovers and registers drivers and apps from service registry
Name: com.objectstack.driver.{name}
Location: packages/runtime/src/driver-plugin.ts
Registers a database driver with ObjectQL.
import { DriverPlugin } from '@objectstack/runtime';
import { createMemoryDriver } from '@objectstack/driver-memory';
const driver = createMemoryDriver();
kernel.use(new DriverPlugin(driver, 'memory'));Services Registered: driver.{name}
Note: Drivers are discovered by ObjectQLPlugin during its start phase.
Name: com.objectstack.app.{name}
Location: packages/runtime/src/app-plugin.ts
Loads an application manifest.
import { AppPlugin } from '@objectstack/runtime';
import manifest from './objectstack.config';
kernel.use(new AppPlugin(manifest));Services Registered: app.{name}
Name: com.objectstack.plugin.hono-server
Location: packages/plugins/plugin-hono-server/src/plugin.ts
Starts an HTTP server using Hono.
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
kernel.use(new HonoServerPlugin({ port: 3000 }));Dependencies: Requires objectql service
Features:
- REST API endpoints
- Middleware support
- CRUD operations
- Query execution
import { Plugin, PluginContext } from '@objectstack/runtime';
export class MyPlugin implements Plugin {
name = 'com.mycompany.my-plugin';
dependencies = ['com.objectstack.engine.objectql'];
async init(ctx: PluginContext): Promise<void> {
ctx.logger.info('MyPlugin initializing');
// Register a service
ctx.registerService('my-service', {
doSomething: () => console.log('Hello!'),
});
// Subscribe to events
ctx.hook('kernel:ready', async () => {
ctx.logger.info('Kernel is ready!');
});
}
async start(ctx: PluginContext): Promise<void> {
ctx.logger.info('MyPlugin starting');
// Get other services
const ql = ctx.getService('objectql');
// Register objects, start servers, etc.
}
async destroy(): Promise<void> {
// Clean up resources
}
}export class AnalyticsPlugin implements Plugin {
name = 'com.mycompany.analytics';
dependencies = [
'com.objectstack.engine.objectql',
'com.mycompany.my-plugin',
];
async init(ctx: PluginContext): Promise<void> {
// This plugin will init AFTER its dependencies
const myService = ctx.getService('my-service');
myService.doSomething();
}
}Plugins communicate via events (hooks).
// Kernel lifecycle
'kernel:init' // Before plugins init
'kernel:ready' // After all plugins start
'kernel:shutdown' // Before shutdown
// Data lifecycle
'data:record:beforeCreate' // { table, data }
'data:record:afterCreate' // { table, record }
'data:record:beforeUpdate' // { table, id, data }
'data:record:afterUpdate' // { table, id, record }
'data:record:beforeDelete' // { table, id }
'data:record:afterDelete' // { table, id }
// Server lifecycle
'server:route:register' // { method, path, handler }
'server:ready' // { port, url }
'server:request' // { method, path, query, body }// Subscribe to events
ctx.hook('data:record:afterCreate', async (event) => {
console.log('Record created:', event.record);
});
// Trigger events
await ctx.trigger('my:custom:event', { data: 'value' });The kernel automatically resolves plugin dependencies using topological sort.
const kernel = new ObjectKernel();
// These will be initialized in dependency order
kernel.use(new DriverPlugin(driver, 'memory')); // depends on ObjectQL
kernel.use(new ObjectQLPlugin()); // no dependencies
kernel.use(new MyAnalyticsPlugin()); // depends on both
await kernel.bootstrap();
// Order: ObjectQL → Driver → Analyticsimport { ObjectKernel } from '@objectstack/runtime';
// Create test kernel
const kernel = new ObjectKernel();
// Register mock ObjectQL
kernel.use({
name: 'objectql-mock',
async init(ctx) {
ctx.registerService('objectql', {
getSchema: () => mockSchema,
query: () => mockData,
});
},
});
// Test your plugin
kernel.use(new MyPlugin());
await kernel.bootstrap();import { describe, it, expect } from 'vitest';
import { ObjectKernel } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
describe('MyPlugin', () => {
it('should register service', async () => {
const kernel = new ObjectKernel();
kernel.use(new ObjectQLPlugin());
kernel.use(new MyPlugin());
await kernel.bootstrap();
const service = kernel.getService('my-service');
expect(service).toBeDefined();
await kernel.shutdown();
});
});Load plugins from a configuration file:
// objectstack.config.ts
export default {
plugins: [
{ name: '@objectstack/objectql#ObjectQLPlugin' },
{ name: '@objectstack/driver-memory#MemoryDriverPlugin' },
{ name: '@objectstack/plugin-hono-server#HonoServerPlugin', config: { port: 3000 } },
],
};// Runtime loader
import { loadConfig } from '@objectstack/config';
import { ObjectKernel } from '@objectstack/runtime';
const config = await loadConfig('./objectstack.config.ts');
const kernel = new ObjectKernel();
for (const pluginDef of config.plugins) {
const PluginClass = await import(pluginDef.name);
kernel.use(new PluginClass(pluginDef.config));
}
await kernel.bootstrap();// ❌ Bad: Direct import
import { objectql } from './global-instance';
// ✅ Good: Get from context
const ql = ctx.getService('objectql');async init(ctx: PluginContext): Promise<void> {
try {
await this.setupDatabase();
} catch (error) {
ctx.logger.error('Failed to setup database', { error });
throw error; // Kernel will halt bootstrap
}
}async destroy(): Promise<void> {
// Close connections
await this.db.close();
// Stop timers
clearInterval(this.syncTimer);
// Unsubscribe from events
this.eventUnsubscribe();
}// ❌ Bad: Generic names
ctx.hook('update', handler);
// ✅ Good: Specific, namespaced names
ctx.hook('analytics:metric:updated', handler);import { ObjectQL } from '@objectstack/objectql';
import { createMemoryDriver } from '@objectstack/driver-memory';
const ql = new ObjectQL();
const driver = createMemoryDriver();
await ql.addDriver(driver, 'memory');
await ql.loadObjects([Account, Contact]);import { ObjectKernel, DriverPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
const kernel = new ObjectKernel();
kernel.use(new ObjectQLPlugin());
kernel.use(new DriverPlugin(driver, 'memory'));
kernel.use(new CRMPlugin()); // Loads Account, Contact
await kernel.bootstrap();
const ql = kernel.getService('objectql');- Writing Plugins - Complete plugin development guide
- ObjectQL Plugin Reference - ObjectQL plugin API
- Server Drivers - Creating custom drivers
Quick Start: See examples/host for a complete MicroKernel implementation.