Plugin System
Complete guide to the ObjectStack plugin architecture — creating, configuring, and managing plugins with the CLI.
Plugin System
ObjectStack is built on a microkernel architecture where nearly everything beyond the core data engine is delivered as a plugin. This guide covers how to create, configure, and manage plugins using the CLI and configuration.
Key Principle: In ObjectStack, the concept of "Project" and "Plugin" is fluid. A Project is simply a Stack that is currently being executed. A Plugin is a Stack loaded by another Stack. Any project can be imported as a plugin without code changes.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ ObjectStack Kernel │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Plugin Loader │ │ Service │ │ Event / Hook │ │
│ │ & Lifecycle │ │ Registry │ │ System │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Plugin Layer │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────┐ │
│ │ ObjectQL │ │ Auth │ │ Hono │ │ Memory │ │
│ │ (data) │ │ (security) │ │ (server) │ │ (driver)│ │
│ └────────────┘ └────────────┘ └────────────┘ └─────────┘ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ REST API │ │ MSW │ │ Custom │ │
│ │ (api) │ │ (testing) │ │ (your own) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘Plugin Types
ObjectStack defines several specialized plugin types:
| Type | Purpose | Example |
|---|---|---|
standard | General-purpose backend logic | Custom services, hooks |
ui | Frontend assets / SPA serving | Studio, Console |
driver | Database or storage adapters | PostgreSQL, Memory |
server | HTTP/RPC server integration | Hono, Express |
app | Vertical solution bundles | CRM, Todo |
theme | UI appearance overrides | Dark theme, Brand theme |
agent | AI autonomous actors | Chat agent, RAG agent |
objectql | Core data provider | ObjectQL engine |
Plugin Interface
Every plugin implements the Plugin interface from @objectstack/core:
interface Plugin {
/** Unique plugin name (e.g., 'com.objectstack.engine.objectql') */
name: string;
/** Plugin version */
version?: string;
/** Plugin type */
type?: 'standard' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql';
/** Plugin dependencies (loaded before this plugin) */
dependencies?: string[];
/** Phase 1: Register services */
init(ctx: PluginContext): Promise<void> | void;
/** Phase 2: Execute business logic (after all plugins initialized) */
start?(ctx: PluginContext): Promise<void> | void;
/** Phase 3: Cleanup on shutdown */
destroy?(): Promise<void> | void;
}Plugin Context
The PluginContext provides access to kernel capabilities:
interface PluginContext {
/** Register a service for other plugins to consume */
registerService(name: string, service: any): void;
/** Get a service registered by another plugin */
getService<T>(name: string): T;
/** Replace an existing service implementation */
replaceService<T>(name: string, implementation: T): void;
/** Get all registered services */
getServices(): Map<string, any>;
/** Register a hook handler */
hook(name: string, handler: (...args: any[]) => void | Promise<void>): void;
/** Trigger a hook */
trigger(name: string, ...args: any[]): Promise<void>;
/** Logger instance */
logger: Logger;
/** Access the kernel instance */
getKernel(): ObjectKernel;
}Plugin Lifecycle
Plugins follow a strict three-phase lifecycle managed by the kernel:
┌──────────────────────┐
│ kernel.use() │
│ Register plugin │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Phase 1: init() │
│ Register services │
│ Set up hooks │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Phase 2: start() │
│ Start servers │
│ Connect databases │
│ Execute logic │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ kernel:ready hook │
│ System operational │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Phase 3: destroy() │
│ Cleanup resources │
│ (reverse order) │
└──────────────────────┘- init() — Called during kernel initialization. Register services that other plugins may depend on.
- start() — Called after all plugins have initialized. Start servers, connect to databases, or execute main logic.
- destroy() — Called during shutdown, in reverse order. Clean up connections, timers, and resources.
Creating a Plugin
Using the CLI
The fastest way to create a plugin is with the CLI scaffolding:
# Create a new plugin project
os create plugin my-feature
# This creates:
# packages/plugins/plugin-my-feature/
# ├── package.json
# ├── tsconfig.json
# ├── README.md
# └── src/
# └── index.tsManual Plugin Creation
import type { Plugin } from '@objectstack/core';
export const myFeaturePlugin: Plugin = {
name: 'com.example.my-feature',
version: '1.0.0',
type: 'standard',
async init(ctx) {
// Register a service that other plugins can consume
const myService = { greet: (name: string) => `Hello, ${name}!` };
ctx.registerService('my-feature', myService);
// Listen for hooks from other plugins
ctx.hook('data:afterInsert', async (object, record) => {
ctx.logger.info(`Record inserted into ${object}`, { record });
});
},
async start(ctx) {
// Access services registered by other plugins
const data = ctx.getService('data');
ctx.logger.info('My feature plugin started');
},
async destroy() {
// Cleanup resources
},
};
export default myFeaturePlugin;Plugin with Dependencies
export const analyticsPlugin: Plugin = {
name: 'com.example.analytics',
version: '1.0.0',
dependencies: ['com.objectstack.server.hono'], // Loaded after Hono
async init(ctx) {
// Safe to access Hono server because of dependency declaration
const server = ctx.getService('http.server');
ctx.registerService('analytics', new AnalyticsService());
},
};Configuring Plugins
Plugins are configured in objectstack.config.ts:
import { defineStack } from '@objectstack/spec';
import authPlugin from '@objectstack/plugin-auth';
import myPlugin from './src/plugins/my-plugin';
export default defineStack({
manifest: {
id: 'com.example.my-app',
namespace: 'my_app',
version: '1.0.0',
type: 'app',
name: 'My App',
},
objects: [...],
// Production plugins
plugins: [
authPlugin,
myPlugin,
],
// Development-only plugins (loaded only with `os dev`)
devPlugins: [
'@objectstack/plugin-msw', // String references also work
],
});Plugin Loading Strategies
The plugins array accepts multiple formats:
| Format | Example | Description |
|---|---|---|
| Plugin instance | new AuthPlugin(config) | Class-based plugin with configuration |
| Plugin object | { name, init, start } | Plain object implementing Plugin interface |
| Package name | '@objectstack/plugin-auth' | String reference, imported at runtime |
| Stack definition | defineStack({...}) | Another project loaded as a plugin |
Managing Plugins with CLI
List Plugins
View all plugins configured in your project:
# List all plugins
os plugin list
# With JSON output
os plugin list --json
# From a specific config file
os plugin list path/to/objectstack.config.tsPlugin Information
Get detailed information about a specific plugin:
os plugin info auth
os plugin info @objectstack/plugin-authAdd a Plugin
Add a plugin to your configuration file:
# Add a production plugin
os plugin add @objectstack/plugin-auth
# Add a dev-only plugin
os plugin add @objectstack/plugin-msw --devThis command:
- Adds an import statement to your config file
- Inserts the plugin into the
plugins(ordevPlugins) array - Reminds you to install the npm package
Remove a Plugin
Remove a plugin from your configuration:
os plugin remove @objectstack/plugin-auth
os plugin rm auth # 'rm' alias also worksBuilt-in Plugins
ObjectStack ships with several official plugins:
@objectstack/plugin-auth
Authentication and identity management powered by better-auth.
- OAuth, 2FA, passkeys, magic links
- Session management
- Depends on:
com.objectstack.server.hono
@objectstack/plugin-hono-server
HTTP server integration using Hono.
- Lightweight and fast
- Provides the
http.serverservice
@objectstack/plugin-security
Security features including field-level and row-level security.
- Permission enforcement
- Middleware-based security
@objectstack/plugin-msw
Mock Service Worker integration for testing.
- Intercept HTTP requests in tests
- Dev-only plugin
@objectstack/driver-memory
In-memory data driver for development and testing.
- Auto-registered in dev mode if no driver is configured
Auto-Detection
The os serve and os dev commands automatically detect and load plugins:
| Condition | Auto-loaded Plugin |
|---|---|
objects defined, no ObjectQL | @objectstack/objectql |
| Dev mode, no driver, has objects | @objectstack/driver-memory |
objects, manifest, or apps defined | AppPlugin (runtime) |
| Server not disabled | @objectstack/plugin-hono-server |
| Server enabled | @objectstack/rest (REST API) |
This means a minimal config like this already works:
export default defineStack({
manifest: { name: 'demo', version: '1.0.0' },
objects: [myObject],
});Running os dev will auto-register ObjectQL, MemoryDriver, AppPlugin, HonoServer, and REST API.
Advanced Topics
Service Replacement
Optimization plugins can replace existing services:
const optimizedCachePlugin: Plugin = {
name: 'com.example.redis-cache',
version: '1.0.0',
async init(ctx) {
// Replace the default in-memory cache with Redis
ctx.replaceService('cache', new RedisCacheService());
},
};Health Checks
Plugins with the PluginMetadata interface can provide health checks:
const dbPlugin: PluginMetadata = {
name: 'com.example.database',
version: '1.0.0',
async healthCheck() {
const isConnected = await db.ping();
return {
healthy: isConnected,
message: isConnected ? 'Database connected' : 'Connection lost',
};
},
async init(ctx) { /* ... */ },
};Plugin Security
ObjectStack supports plugin security features:
- Configuration Validation: Plugins can define a Zod
configSchemafor runtime validation - Signature Verification: Cryptographic signatures for plugin integrity
- Permission Enforcement: Fine-grained access control for plugin operations
import { z } from 'zod';
const securePlugin: PluginMetadata = {
name: 'com.example.secure',
version: '1.0.0',
configSchema: z.object({
apiKey: z.string().min(1),
region: z.enum(['us', 'eu', 'ap']),
}),
signature: 'sha256:abc123...',
async init(ctx) { /* ... */ },
};Startup Timeout
Plugins can configure a custom startup timeout (default: 30 seconds):
const slowPlugin: PluginMetadata = {
name: 'com.example.slow-init',
version: '1.0.0',
startupTimeout: 60000, // 60 seconds
async init(ctx) {
// Long initialization...
},
};CLI Command Extensions
Plugins can extend the ObjectStack CLI (os) with custom commands. This enables third-party packages — such as marketplace tools, deployment utilities, or domain-specific workflows — to register new top-level subcommands.
How It Works
┌──────────────────────────────────────────────────────────┐
│ os (CLI) │
│ ┌────────────┐ ┌────────────┐ ┌─────────────────────┐ │
│ │ Built-in │ │ Built-in │ │ Plugin Commands │ │
│ │ init, dev │ │ plugin, │ │ marketplace, │ │
│ │ compile .. │ │ generate │ │ deploy, ... │ │
│ └────────────┘ └────────────┘ └─────────────────────┘ │
│ ▲ │
│ │ │
│ loadPluginCommands() │
│ reads objectstack.config.ts │
│ discovers contributes.commands │
│ dynamically imports modules │
└──────────────────────────────────────────────────────────┘- The plugin declares
contributes.commandsin its manifest. - The CLI scans installed plugins at startup via
loadPluginCommands(). - Each plugin module is dynamically imported and its exported Commander.js
Commandinstances are registered.
Step 1: Declare Commands in the Manifest
Add a contributes.commands section to the plugin's manifest:
// objectstack.config.ts (plugin package)
import { defineStack } from '@objectstack/spec';
export default defineStack({
manifest: {
id: 'com.acme.marketplace',
namespace: 'marketplace',
version: '1.0.0',
type: 'plugin',
name: 'Marketplace Plugin',
contributes: {
commands: [
{
name: 'marketplace',
description: 'Manage marketplace applications',
module: './dist/cli.js', // optional, defaults to package main
},
],
},
},
});Each command entry has:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | CLI command name (lowercase, hyphens allowed) |
description | string | optional | Help text shown in os --help |
module | string | optional | Module path exporting Commander.js commands |
Step 2: Implement the CLI Module
Create a module that exports Commander.js Command instances. The CLI supports three export forms:
// src/cli.ts
import { Command } from 'commander';
// ── Subcommands ──────────────────────────────────────────
const publishCommand = new Command('publish')
.description('Publish an app to the marketplace')
.argument('<package>', 'Package to publish')
.option('--public', 'Make publicly visible')
.action(async (pkg: string, options: { public?: boolean }) => {
console.log(`Publishing ${pkg}...`);
// ... publish logic
});
const searchCommand = new Command('search')
.description('Search marketplace apps')
.argument('<query>', 'Search query')
.option('-l, --limit <n>', 'Max results', '10')
.action(async (query: string, options: { limit: string }) => {
console.log(`Searching for "${query}"...`);
// ... search logic
});
const installCommand = new Command('install')
.description('Install an app from the marketplace')
.argument('<app>', 'App identifier')
.action(async (app: string) => {
console.log(`Installing ${app}...`);
// ... install logic
});
// ── Main Command ─────────────────────────────────────────
const marketplaceCommand = new Command('marketplace')
.description('Manage marketplace applications')
.addCommand(publishCommand)
.addCommand(searchCommand)
.addCommand(installCommand);
// Export as named array (recommended)
export const commands = [marketplaceCommand];
// Alternative: export default (single command or array)
// export default marketplaceCommand;
// export default [marketplaceCommand, anotherCommand];Step 3: Register the Plugin
Add the plugin to the host project's objectstack.config.ts:
// Host project objectstack.config.ts
import { defineStack } from '@objectstack/spec';
import marketplacePlugin from '@acme/plugin-marketplace';
export default defineStack({
manifest: {
id: 'com.example.my-app',
version: '1.0.0',
type: 'app',
name: 'My App',
},
plugins: [
marketplacePlugin,
],
});Or use the CLI to add it:
os plugin add @acme/plugin-marketplace
pnpm add @acme/plugin-marketplaceUsing the Extended CLI
Once installed, the new commands appear in os --help and can be invoked directly:
# List available commands (includes plugin commands)
os --help
# Use marketplace commands
os marketplace search "crm"
os marketplace install com.acme.crm
os marketplace publish ./dist --publicComplete Example: Deployment Plugin
Here's a full example of a deployment CLI plugin:
// @acme/plugin-deploy/src/cli.ts
import { Command } from 'commander';
const deployCommand = new Command('deploy')
.description('Deploy ObjectStack app to cloud')
.addCommand(
new Command('staging')
.description('Deploy to staging environment')
.option('--skip-tests', 'Skip pre-deploy tests')
.action(async (options) => {
console.log('Deploying to staging...');
if (!options.skipTests) {
console.log('Running tests first...');
}
// deploy logic
})
)
.addCommand(
new Command('production')
.description('Deploy to production')
.option('--confirm', 'Skip confirmation prompt')
.action(async (options) => {
if (!options.confirm) {
// prompt for confirmation
}
console.log('Deploying to production...');
})
)
.addCommand(
new Command('status')
.description('Check deployment status')
.action(async () => {
console.log('Checking deployment status...');
})
);
export const commands = [deployCommand];// @acme/plugin-deploy/objectstack.config.ts
import { defineStack } from '@objectstack/spec';
export default defineStack({
manifest: {
id: 'com.acme.deploy',
version: '1.0.0',
type: 'plugin',
name: 'Deploy Plugin',
contributes: {
commands: [
{
name: 'deploy',
description: 'Deploy ObjectStack app to cloud',
module: './dist/cli.js',
},
],
},
},
});Usage:
os deploy staging --skip-tests
os deploy production --confirm
os deploy statusGraceful Degradation: If a plugin command fails to load (e.g., missing dependency), the CLI logs a warning in DEBUG mode and continues with built-in commands. Plugin commands never block the CLI.
Directory Structure Convention
ObjectStack defines a Plugin Structure Standard (OPS) for consistent project layout:
plugin-my-feature/
├── package.json
├── tsconfig.json
├── objectstack.config.ts # Optional: if plugin is also a Stack
├── README.md
├── CHANGELOG.md
└── src/
├── index.ts # Entry point (required)
├── cli.ts # CLI commands (optional, for contributes.commands)
├── my_feature/ # Domain folder (snake_case)
│ ├── feature.object.ts # Business object
│ ├── feature.view.ts # View definition
│ └── index.ts # Barrel export
└── services/
└── feature.service.ts # Service implementationNaming Rules:
- Domain directories:
snake_case - File suffixes:
.object.ts,.view.ts,.flow.ts,.service.ts, etc. - Entry point:
index.tsin every module
CLI Plugin Commands Reference
| Command | Alias | Description |
|---|---|---|
os plugin list [config] | os plugin ls | List all configured plugins |
os plugin info <name> [config] | — | Show detailed plugin information |
os plugin add <package> | — | Add a plugin to config |
os plugin remove <name> | os plugin rm | Remove a plugin from config |
Options
| Option | Commands | Description |
|---|---|---|
--json | list | Output as JSON |
--dev | add | Add as a dev-only plugin |
-c, --config <path> | add, remove | Specify config file path |
Next Steps
- Data Modeling — Define business objects and fields
- Authentication — Set up auth with the auth plugin
- Kernel Services — Understand the service registry
- Security — Configure field and row-level security