ObjectStackObjectStack

Plugin system — extend the runtime safely

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

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

Which artifact this page means. Every plugin taught here is a kernel code plugin — TypeScript implementing the Plugin contract, built by tsc. That is what os create plugin scaffolds: by default an unscoped plugin-<name> package marked "private": true, or a publishable @objectstack/plugin-<name> under --in-repo inside a checkout of the ObjectStack monorepo. If what you want instead is a metadata package — declarative objects another stack loads, with no kernel code — the scaffolder is os init <name> -t plugin, and Which scaffolder? routes between the two by artifact rather than by the word plugin, which the CLI spells the same for both.

The fastest way to create a kernel code plugin is with the CLI scaffolding:

# Create a new kernel code plugin project in the current directory
os create plugin my-feature

# This creates:
# plugin-my-feature/
# ├── package.json
# ├── tsconfig.json
# ├── README.md
# ├── pnpm-workspace.yaml
# └── src/
#     └── index.ts

cd plugin-my-feature
pnpm install
pnpm build

The scaffold is a standalone project: its package.json depends on the published @objectstack/* releases that match the CLI which generated it, and its tsconfig.json extends nothing outside the project — so it installs and builds wherever you put it. Add --in-repo only when you are scaffolding into a checkout of the ObjectStack monorepo itself; that placement emits workspace:* dependencies and installs nowhere else.

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 testing and embedding.

  • Not a boot store: OS_DATABASE_DRIVER=memory, --database-driver memory and a memory:// URL are refused at boot, because the driver refuses every tenant-scoped read. For a throwaway dev database use os dev --fresh; for SQLite's own in-memory database use OS_DATABASE_URL=:memory:
  • Still built for a declared driver: 'memory' datasource and by direct construction (new InMemoryDriver())

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:

  • Signature Verification: Cryptographic signatures for plugin integrity
  • Permission Enforcement: Fine-grained access control for plugin operations

Plugin configuration is the plugin's own concern: take it as a constructor or factory argument and parse it at your own seam (a Zod schema.parse in the factory, constructor or init). The kernel-side configSchema metadata field was retired under ADR-0049 — the kernel never received a plugin's config, so the field could not validate anything.

const securePlugin: PluginMetadata = {
  name: 'com.example.secure',
  version: '1.0.0',
  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

@objectstack/cli ships no plugin manager. Its package.json declares no oclif.plugins, and it does not depend on @oclif/plugin-plugins or @oclif/plugin-help. So os plugins … (install, link, update, …) and os help are not commands: each exits 2 with command … not found. os --help is the help entry, and it shows no plugins topic. To load a CLI extension, build an os distribution: a package whose own package.json lists the extension in both oclif.plugins and dependencies. oclif's core-plugin loader matches oclif.plugins names only against dependencies, so a name listed under devDependencies alone never loads.

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