ObjectStackObjectStack

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:

TypePurposeExample
standardGeneral-purpose backend logicCustom services, hooks
uiFrontend assets / SPA servingStudio, Console
driverDatabase or storage adaptersPostgreSQL, Memory
serverHTTP/RPC server integrationHono, Express
appVertical solution bundlesCRM, Todo
themeUI appearance overridesDark theme, Brand theme
agentAI autonomous actorsChat agent, RAG agent
objectqlCore data providerObjectQL 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)     │
                └──────────────────────┘
  1. init() — Called during kernel initialization. Register services that other plugins may depend on.
  2. start() — Called after all plugins have initialized. Start servers, connect to databases, or execute main logic.
  3. 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.ts

Manual 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:

FormatExampleDescription
Plugin instancenew 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 definitiondefineStack({...})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.ts

Plugin Information

Get detailed information about a specific plugin:

os plugin info auth
os plugin info @objectstack/plugin-auth

Add 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 --dev

This command:

  1. Adds an import statement to your config file
  2. Inserts the plugin into the plugins (or devPlugins) array
  3. 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 works

Built-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.server service

@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:

ConditionAuto-loaded Plugin
objects defined, no ObjectQL@objectstack/objectql
Dev mode, no driver, has objects@objectstack/driver-memory
objects, manifest, or apps definedAppPlugin (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 configSchema for 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            │
└──────────────────────────────────────────────────────────┘
  1. The plugin declares contributes.commands in its manifest.
  2. The CLI scans installed plugins at startup via loadPluginCommands().
  3. Each plugin module is dynamically imported and its exported Commander.js Command instances 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:

PropertyTypeRequiredDescription
namestringCLI command name (lowercase, hyphens allowed)
descriptionstringoptionalHelp text shown in os --help
modulestringoptionalModule 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-marketplace

Using 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 --public

Complete 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 status

Graceful 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 implementation

Naming Rules:

  • Domain directories: snake_case
  • File suffixes: .object.ts, .view.ts, .flow.ts, .service.ts, etc.
  • Entry point: index.ts in every module

CLI Plugin Commands Reference

CommandAliasDescription
os plugin list [config]os plugin lsList 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 rmRemove a plugin from config

Options

OptionCommandsDescription
--jsonlistOutput as JSON
--devaddAdd as a dev-only plugin
-c, --config <path>add, removeSpecify config file path

Next Steps

On this page