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
- 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
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 buildThe 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:
| 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 testing and embedding.
- Not a boot store:
OS_DATABASE_DRIVER=memory,--database-driver memoryand amemory://URL are refused at boot, because the driver refuses every tenant-scoped read. For a throwaway dev database useos dev --fresh; for SQLite's own in-memory database useOS_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:
| 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:
- 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.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