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
- Plugin Anatomy — The
Plugincontract: interface,PluginContext, and lifecycle phases - Plugin Development — Step-by-step tutorial: build, test, and register a plugin
- Package Overview — All ObjectStack packages, services, drivers, plugins, and adapters
- Adding a Metadata Type — Register a new metadata type in the Studio Metadata Admin
- Schema reference: Kernel
- Spec: Plugin Package Specification
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:
| 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 — 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.tsFor 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:
| 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
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-authWhen 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.serverservice
@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 amemory://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:
| Condition | Auto-loaded Plugin |
|---|---|
objects defined, no ObjectQL | @objectstack/objectql |
objects defined, no driver | DefaultDatasourcePlugin (runtime) — driver kind from OS_DATABASE_DRIVER / OS_DATABASE_URL, defaulting to SQLite |
objects, manifest, apps, flows, or apis defined | AppPlugin (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
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: '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.tsin every module
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