For comprehensive documentation with incorrect/correct examples:
- Plugin Lifecycle — 3-phase lifecycle (init/start/destroy), execution order, complete examples
- Service Registry — DI container, factories, lifecycles (singleton/transient/scoped), core fallbacks
- Hooks & Events — Kernel hooks & events reference (record-level lifecycle hooks → objectstack-data)
| Feature | ObjectKernel | LiteKernel |
|---|---|---|
| Use case | Production servers, full applications | Serverless, edge, unit tests |
| Package | @objectstack/core |
@objectstack/core |
| Plugin loading | Async with validation & metadata | Synchronous use() |
| Service factories | Singleton / Transient / Scoped | Direct instances only |
| Health monitoring | Built-in per-plugin health checks | Not available |
| Graceful shutdown | Timeout + rollback on failure | Basic destroy phase |
| Dependency resolution | Topological sort + circular detection (throws) | Topological sort (throws on cycles) |
| Core fallbacks | Auto-injects in-memory fallbacks | Not available |
| Config validation | Zod schema validation per plugin | Not available |
If the host only needs the data engine — query / CRUD / hooks / validation —
neither kernel is the answer. Import ObjectQL from
@objectstack/objectql/core (ADR-0076): no kernel, no ObjectQLPlugin, no
metadata-management layer, and the same ObjectSchema.create({ … })
definitions a full backend ships. examples/embed-objectql is the worked
example; it is the right shape for a thin, latency-sensitive host such as a
gateway.
import { ObjectKernel } from '@objectstack/core';
const kernel = new ObjectKernel({
logger: {
level: 'info', // debug|info|warn|error|fatal|silent
format: 'json', // 'json' | 'text' | 'pretty'
},
defaultStartupTimeout: 30000, // Per plugin (ms)
gracefulShutdown: true, // Register SIGINT/SIGTERM handlers
shutdownTimeout: 60000, // Total shutdown timeout (ms)
rollbackOnFailure: true, // Rollback all plugins if one fails
skipSystemValidation: false, // Skip system checks (useful for tests)
});import { LiteKernel } from '@objectstack/core';
const kernel = new LiteKernel({
logger: { level: 'warn' },
});import type { Plugin, PluginContext } from '@objectstack/core';
export interface Plugin {
name: string; // Unique identifier (reverse domain recommended)
version?: string; // Semantic version
type?: PluginType; // closed set exported by @objectstack/core
dependencies?: string[]; // Plugins that must init before this one
// Phase 1: Register services
init(ctx: PluginContext): Promise<void> | void;
// Phase 2: Execute business logic (optional)
start?(ctx: PluginContext): Promise<void> | void;
// Phase 3: Cleanup (optional)
destroy?(): Promise<void> | void;
}See rules/plugin-lifecycle.md for complete examples.
// Register a service (in init phase)
ctx.registerService('my-service', myServiceInstance);
// Get a service (in start phase)
const db = ctx.getService<IDataEngine>('objectql');
// Replace a service
ctx.replaceService('cache', new InstrumentedCache(existingCache));
// Get all services
const allServices: Map<string, any> = ctx.getServices();See rules/service-registry.md for factories and lifecycles.
// Register a kernel hook handler
ctx.hook('kernel:ready', async () => {
ctx.logger.info('System is ready!');
});
// React to a metadata hot-reload / publish
ctx.hook('metadata:reloaded', async (payload?: { changed?: string[] }) => {
ctx.logger.info('Metadata reloaded', { changed: payload?.changed });
});
// Trigger a custom hook
await ctx.trigger('my-plugin:initialized', { version: '1.0.0' });Built-in kernel events: kernel:ready, kernel:bootstrapped,
kernel:listening, kernel:shutdown, app:seeded, metadata:reloaded,
external.schema.drift.
⚠️ There are nodata:*kernel hooks. Record-level lifecycle logic (beforeInsert / afterUpdate / …) runs on the ObjectQL engine, not the kernel event bus — author it via thehooks:collection orql.on('beforeInsert', 'task', async (ctx) => { … })(see objectstack-data). Becausectx.hook()accepts any string, a handler registered for'data:beforeInsert'will register successfully and then silently never fire. Kernel hooks are for platform lifecycle only.
See references/plugin-hooks.md for the kernel event list, payloads, and patterns.
ctx.logger.debug('Detailed trace info', { key: 'value' });
ctx.logger.info('Plugin initialized');
ctx.logger.warn('Cache miss rate high', { rate: 0.45 });
ctx.logger.error('Connection failed', error);const kernel = ctx.getKernel();
const isRunning = kernel.isRunning();
const state = kernel.getState(); // 'idle' | 'initializing' | 'running' | 'stopping' | 'stopped'// src/plugins/audit.ts
import type { Plugin, PluginContext } from '@objectstack/core';
interface AuditEntry {
timestamp: string;
event: string;
detail?: Record<string, unknown>;
}
class AuditService {
private log: AuditEntry[] = [];
record(event: string, detail?: Record<string, unknown>) {
this.log.push({ timestamp: new Date().toISOString(), event, detail });
}
getLog(): AuditEntry[] {
return [...this.log];
}
}
const AuditPlugin: Plugin = {
name: 'com.example.audit',
version: '1.0.0',
type: 'plugin',
async init(ctx: PluginContext) {
// Phase 1: Register service and kernel hooks
const auditService = new AuditService();
ctx.registerService('audit', auditService);
ctx.hook('kernel:ready', async () => {
auditService.record('kernel:ready');
});
ctx.hook('metadata:reloaded', async (payload?: { changed?: string[] }) => {
auditService.record('metadata:reloaded', { changed: payload?.changed });
});
ctx.logger.info('Audit plugin initialized');
},
async start(ctx: PluginContext) {
// Phase 2: Log that audit is active
ctx.logger.info('Audit logging active');
},
async destroy() {
// Phase 3: Cleanup
},
};
export default AuditPlugin;import { ObjectKernel } from '@objectstack/core';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { DriverPlugin } from '@objectstack/runtime';
import { InMemoryDriver } from '@objectstack/driver-memory';
import AuditPlugin from './plugins/audit';
const kernel = new ObjectKernel();
await kernel.use(new ObjectQLPlugin());
await kernel.use(new DriverPlugin(new InMemoryDriver()));
await kernel.use(AuditPlugin);
await kernel.bootstrap();
// Services are now available
const audit = kernel.getService('audit');import { describe, it, expect } from 'vitest';
import { LiteKernel } from '@objectstack/core';
import type { PluginContext } from '@objectstack/core';
import AuditPlugin from './audit';
describe('AuditPlugin', () => {
it('records kernel lifecycle events', async () => {
const kernel = new LiteKernel({ logger: { level: 'silent' } });
kernel.use(AuditPlugin);
// `kernel.context` is protected — to fire events in a test, capture a
// PluginContext from a probe plugin instead.
let probe!: PluginContext;
kernel.use({ name: 'test.probe', init(ctx) { probe = ctx; } });
await kernel.bootstrap(); // fires kernel:ready → recorded
// Simulate a metadata hot-reload announcement
await probe.trigger('metadata:reloaded', { changed: ['object/task'] });
const audit = kernel.getService<{ getLog(): { event: string }[] }>('audit');
const events = audit.getLog().map((e) => e.event);
expect(events).toContain('kernel:ready');
expect(events).toContain('metadata:reloaded');
await kernel.shutdown();
});
});| Plugin Name | Service Key | Package |
|---|---|---|
com.objectstack.engine.objectql |
objectql (also data) |
@objectstack/objectql |
com.objectstack.driver.* |
driver.{name} |
@objectstack/driver-* |
com.objectstack.auth |
auth |
@objectstack/plugin-auth |
com.objectstack.rest.api |
— (registers no service) | @objectstack/rest |
com.objectstack.metadata |
metadata |
@objectstack/metadata |
com.objectstack.service.realtime |
realtime |
@objectstack/service-realtime |
com.objectstack.service.cache |
cache |
@objectstack/service-cache |
com.objectstack.server.hono |
— | @objectstack/plugin-hono-server → HonoServerPlugin |
com.objectstack.setup |
— | @objectstack/setup → createSetupAppPlugin (ADR-0048 one-app pkg) |
com.objectstack.studio |
— | @objectstack/studio → createStudioAppPlugin |
com.objectstack.account |
— | @objectstack/account → createAccountAppPlugin |
com.objectstack.cloud.connection |
— | @objectstack/cloud-connection → createCloudConnectionPlugin |
MetadataPlugin is the IMetadataService provider for the ObjectStack runtime, but runtime
metadata is read-only and artifact/file backed:
- Do not register
sys_metadataorsys_metadata_historyfrom an ObjectStack runtime plugin. Those persistence tables belong to the control plane. (Exception: an isolated environment kernel may opt intosys_metadatahydration from its own DB — the general boundary otherwise stands.) - Do not call
MetadataManager.setDataEngine()automatically fromMetadataPlugin.start(). Project databases must contain business rows only. - Use
artifactSource: { mode: 'local-file', path: './dist/objectstack.json' }for local artifact boot; production should use the Artifact API loader once wired. DatabaseLoader,setDatabaseDriver(), andsetDataEngine()remain valid for control-plane services that explicitly own metadata revisions, history, or overlays.
import { MetadataPlugin } from '@objectstack/metadata';
await kernel.use(new MetadataPlugin({
watch: false,
artifactSource: { mode: 'local-file', path: './dist/objectstack.json' },
}));A plugin opts in by adding an async healthCheck() returning
{ healthy: boolean; message?: string; details?: Record<string, unknown> }
(PluginHealthStatus, importable from @objectstack/core). Return healthy: false rather than throwing. The kernel side is three calls:
const health = await kernel.checkPluginHealth('com.example.db');
const allHealth = await kernel.checkAllPluginsHealth();
const metrics = kernel.getPluginMetrics(); // Map<name, startup ms>Feature flags are not a spec/metadata concept. There is no featureFlags: /
features: key on defineStack (writing one is refused at load, not stripped), and the
former FeatureFlagSchema (@objectstack/spec/kernel) was removed — it had zero runtime
consumers, and its only protocol home (the static ObjectStackCapabilities.system.features
descriptor) was itself dead: no endpoint ever served it. Runtime capability discovery is
GET /api/v1/discovery.
The live toggle surfaces are runtime configuration, not authored metadata:
- Org toggles — no shared flags namespace exists; a plugin that needs one declares
its own
SettingsManifestand registers it on thesettingsservice (ADR-0007 cascade). - Auth capability gates —
requiresFeatureon actions/params lowers to thePUBLIC_AUTH_FEATURESregistry (kernel/public-auth-features.ts), the fixed deployment-level flags plugin-auth advertises to anonymous clients.