ObjectStackObjectStack

Plugin System

Overview of the ObjectStack plugin architecture — plugin types, configuration, management, and built-in plugins.

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.

What's in this module


Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    ObjectStack Kernel                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Plugin Loader │  │ Service      │  │ Event / Hook     │  │
│  │ & Lifecycle   │  │ Registry     │  │ System           │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                    Plugin Layer                              │
│  ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────┐ │
│  │ ObjectQL   │ │ Auth       │ │ Hono       │ │ Memory  │ │
│  │ (data)     │ │ (security) │ │ (server)   │ │ (driver)│ │
│  └────────────┘ └────────────┘ └────────────┘ └─────────┘ │
│  ┌────────────┐ ┌────────────┐ ┌────────────┐             │
│  │ REST API   │ │ Dev        │ │ 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 — see Plugin Anatomy for the full contract and the PluginContext reference.


Plugin Lifecycle

Plugins follow a strict three-phase lifecycle (init()start()destroy()) managed by the kernel — see Plugin Anatomy for the phase model.


Creating a Plugin

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

For the full walkthrough — implementing the Plugin interface, registering services and hooks, testing, and registering with the kernel — see the Plugin Development tutorial.


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: [
    new AuthPlugin(),
    myPlugin,
  ],

  // Development-only plugins (loaded only with `os dev` / `os serve --dev`)
  devPlugins: [
  ],
});

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

Runtime plugins are declared directly in objectstack.config.ts under the plugins (or devPlugins) array. Edit the file to add, remove, or reorder plugins — there is no dedicated CLI subcommand for this in v1.

import { defineStack } from '@objectstack/spec';
import { AuthPlugin } from '@objectstack/plugin-auth';

export default defineStack({
  manifest: { /* ... */ },
  plugins: [
    new AuthPlugin({
      socialProviders: {
        github: { clientId: '...', clientSecret: '...' },
      },
    }),
    // add more plugins here
  ],
  devPlugins: [
    // dev-only plugins (loaded by `os dev` / `os serve --dev`)
  ],
});

Install the npm package alongside the config edit:

pnpm add @objectstack/plugin-auth

When you run os dev or os serve, the kernel discovers and loads every plugin listed in the config — no separate registration step needed.


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.engine.objectql

@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/driver-memory

In-memory (mingo) data driver for development and testing.

  • Opt in explicitly with OS_DATABASE_DRIVER=memory, --database-driver memory, or a memory:// URL — when no driver is configured the dev default is SQLite, not this driver

Auto-Detection

The os serve and os dev commands automatically detect and load plugins:

ConditionAuto-loaded Plugin
objects defined, no ObjectQL@objectstack/objectql
objects defined, no driverDefaultDatasourcePlugin (runtime) — driver kind from OS_DATABASE_DRIVER / OS_DATABASE_URL, defaulting to SQLite
objects, manifest, apps, flows, or apis definedAppPlugin (runtime)
Server not disabled@objectstack/plugin-hono-server
Server enabled@objectstack/rest (REST API)

Which SQLite default you get depends on the boot path. A bare defineStack() config — one whose plugins array instantiates nothing — boots through createStandaloneStack, which anchors a SQLite file under the project (.objectstack/data/standalone.db) in development and production. A host config that already instantiates plugins takes serve's own fallback instead: with no OS_DATABASE_URL / OS_DATABASE_DRIVER set it declares SQLite :memory: in dev and registers no datasource in production, so a missing driver surfaces loudly downstream. os dev sidesteps both defaults by exporting OS_DATABASE_URL=file:<project>/.objectstack/data/dev.db.

This means a minimal config like this already works:

export default defineStack({
  manifest: { id: 'com.example.demo', type: 'app', name: 'demo', version: '1.0.0' },
  objects: [myObject],
});

Running os dev will auto-register ObjectQL, the default datasource (a project-anchored SQLite file at .objectstack/data/dev.db), AppPlugin, HonoServer, and the 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: 'ed25519:key-1:<base64url-signature>',
  
  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.

The CLI is built on oclif. Command extension is handled entirely by oclif's plugin system: a plugin ships oclif Command classes, declares oclif config in its package.json, and is loaded by the os binary as an oclif plugin. The host project's objectstack.config.ts does not determine CLI command availability.

Step 1: Add oclif Config to package.json

The plugin package declares its CLI commands location and oclif metadata:

{
  "name": "@acme/plugin-marketplace",
  "version": "1.0.0",
  "type": "module",
  "files": ["dist"],
  "oclif": {
    "commands": {
      "strategy": "pattern",
      "target": "./dist/commands"
    }
  }
}

Step 2: Implement oclif Command Classes

Place command classes under src/commands/ (compiled to dist/commands/). The file path determines the command name — commands/marketplace/search.ts becomes os marketplace search.

// src/commands/marketplace/search.ts
import { Args, Command, Flags } from '@oclif/core';

export default class MarketplaceSearch extends Command {
  static description = 'Search marketplace apps';

  static args = {
    query: Args.string({ description: 'Search query', required: true }),
  };

  static flags = {
    limit: Flags.integer({ char: 'l', description: 'Max results', default: 10 }),
  };

  async run(): Promise<void> {
    const { args, flags } = await this.parse(MarketplaceSearch);
    this.log(`Searching for "${args.query}" (limit ${flags.limit})...`);
    // ... search logic
  }
}

Step 3: Load the Plugin into the CLI

os plugins install is not available today. @objectstack/cli's package.json lists @oclif/plugin-plugins under oclif.plugins, but the package sits in devDependencies — and oclif's core-plugin loader only matches names that appear in dependencies. The result is that neither os plugins … nor os help is a registered command; os --help shows no plugins topic. Until that is fixed, load a CLI extension by building an os distribution that lists the package in both oclif.plugins and dependencies.

Using the Extended CLI

Once loaded, 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"

Directory Structure Convention

Plugins follow the ObjectStack Plugin Standards (OPS) for a 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)
    ├── commands/               # oclif CLI commands (optional, for CLI extension)
    ├── 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 (plain module)

Naming Rules:

  • Domain directories: snake_case
  • Semantic metadata suffixes: .object.ts, .field.ts, .view.ts, .page.ts, .dashboard.ts, .flow.ts, .app.ts. ADR-0088 retired .trigger.ts, .router.ts, .function.ts, and .service.ts — plain source modules (services, helpers) simply carry no semantic suffix.
  • Entry point: index.ts in every module

Next Steps

On this page