Kernel: The System Protocol
The runtime orchestration layer - Lifecycle, plugins, configuration, and system services
Kernel is ObjectStack's runtime orchestration layer that manages the complete lifecycle of the platform. It provides the "operating system" services that coordinate ObjectQL (data) and ObjectUI (interface) into a cohesive application runtime.
The Core Problem
Traditional enterprise platforms tightly couple infrastructure concerns with business logic:
- Deployment Hell: Change one configuration file? Rebuild entire monolith, redeploy all services, pray nothing breaks
- Plugin Chaos: Want to add Stripe billing? Write custom integration code, manage dependencies manually, debug version conflicts
- Configuration Drift: Dev environment uses JSON files, staging uses environment variables, prod uses database config. Which is the source of truth?
- Lifecycle Management: Installing a package requires 47-step runbook: create database tables, run migrations, configure permissions, restart services
- Multi-Tenancy Nightmare: Each customer tenant needs isolated configuration, but they all run the same codebase. How do you manage 500 tenant-specific settings?
Result: DevOps teams spend 60% of their time on deployment mechanics instead of building features. Configuration errors cause 80% of production incidents.
The Kernel Solution
Declarative Lifecycle
Define system state in manifests. Kernel ensures runtime matches declaration—no manual runbooks.
Plugin Isolation
Microkernel architecture: Core runtime <10MB. All features are plugins with dependency management.
Unified Configuration
Single source of truth: Merge environment vars, config files, tenant settings, user preferences—deterministically.
Built-in i18n
Multi-language support at the platform level. No library integration, no translation service APIs.
Architecture Overview
┌──────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ObjectQL (Data) + ObjectUI (Interface) + Business Logic │
└─────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────▼────────────────────────────────────────┐
│ Kernel │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ Lifecycle Mgmt │ │ Plugin Registry │ │ Config Resolver │ │
│ │ Boot/Install │ │ Dependencies │ │ Merge Strategy │ │
│ │ Upgrade │ │ Versioning │ │ Multi-tenant │ │
│ └────────────────┘ └────────────────┘ └──────────────────┘ │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ i18n Engine │ │ Event Bus │ │ Job Scheduler │ │
│ │ Translations │ │ Pub/Sub │ │ Cron/Interval │ │
│ └────────────────┘ └────────────────┘ └──────────────────┘ │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ Audit Logger │ │ Secret Store │ │ Tenant Isolator │ │
│ │ Change Track │ │ Encryption │ │ Multi-org │ │
│ └────────────────┘ └────────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PostgreSQL │ │ Redis │
│ (Primary) │ │ (Cache) │
└──────────────┘ └──────────────┘Key Insight: Kernel is the control plane. ObjectQL and ObjectUI are the data plane. Separating concerns allows independent evolution.
Core Components
Lifecycle Management
Boot sequence, plugin installation, zero-downtime upgrades, rollback strategies
Plugin Specification
Manifest structure, dependency graph, versioning semantics, distribution format
Configuration Resolution
Merge strategies, precedence rules, environment overrides, tenant isolation
i18n Standard
Translation bundles, locale resolution, date/number formatting, dynamic loading
Why the Kernel Exists
Problem: Deployment Complexity Kills Agility
Traditional Approach:
# 47-step deployment runbook
1. Pull latest code
2. Run database migrations (hope they don't fail)
3. Update config.yaml (which version?)
4. Restart service 1, wait 30s
5. Restart service 2, wait 30s
...
47. Pray to the demo godsKernel Approach:
# Publish a compiled artifact as a versioned package to ObjectOS Cloud
os package publish dist/objectstack.json --env <environment-id> --install
# ✓ Validated dependencies
# ✓ Applied schema changes
# ✓ Updated configuration
# ✓ Zero-downtime restart
# Done in 12 seconds.Business Value: A SaaS company reduced deployment time from 45 minutes (with 20% failure rate) to 90 seconds (with 0.1% failure rate). They now deploy 10x per day instead of weekly.
Problem: Plugin Dependency Hell
Traditional Approach:
// package.json
"dependencies": {
"stripe": "^10.0.0", // Need 10.x
"accounting-plugin": "*", // This needs stripe@9.x
// 💥 Conflict! Manual resolution required.
}Kernel Approach:
# manifest block in objectstack.config.ts (via defineStack)
id: com.mycompany.billing
version: 2.0.0
dependencies:
'@objectstack/core': '^2.0.0'
'com.stripe.plugin': '>=10.0.0 <11.0.0'Kernel validates the entire dependency graph at install time. Incompatible plugins fail installation with clear error messages, not runtime crashes.
Business Value: A marketplace with 200+ plugins has zero "it works on my machine" issues. Dependency conflicts are caught before customers hit "Install."
Problem: Configuration Management Chaos
Traditional Approach:
// Where is the source of truth?
const apiKey =
process.env.API_KEY || // Environment variable
config.get('stripe.apiKey') || // Config file
tenant.settings.apiKey || // Database
'fallback-key'; // 💀 Hardcoded defaultKernel Approach:
// One resolver, one precedence order — the `settings` service
// (SettingsService, registered by SettingsServicePlugin)
const settings = ctx.getService<any>('settings');
const { value, source, locked } = await settings.get('stripe', 'apiKey', {
tenantId,
userId,
});
// Cascade: OS_STRIPE_APIKEY (env) > global > tenant > user > registered default.
// `source` names the winning scope; `locked: true` means a higher scope pinned it
// (a write to a locked key throws SettingsLockedError).Business Value: Configuration errors dropped 90% after adopting Kernel. New engineers can understand config logic in 5 minutes instead of 5 hours.
Real-World Use Cases
Multi-Tenant SaaS Platform
Challenge: A B2B SaaS company serves 500 enterprise customers. Each customer needs custom:
- Branding (logo, colors)
- Feature flags (Customer A has AI, Customer B doesn't)
- Integrations (Customer A uses Salesforce, Customer B uses HubSpot)
- Language (Customer A is US English, Customer B is German)
Kernel Solution:
- Tenant Isolation: Each customer is a "tenant" with scoped configuration
- Config Merging: Global defaults → Tenant overrides → User preferences
- Plugin System: Each integration is a plugin; tenants enable only what they need
- i18n Engine: Language bundles loaded per user session
Results:
- Onboard new enterprise customer in 1 hour (previously 2 weeks)
- Zero cross-tenant data leaks (config isolation enforced by runtime)
- 40% reduction in support tickets (consistent config management)
Marketplace Ecosystem
Challenge: Build a plugin marketplace like Salesforce AppExchange where third-party developers sell integrations.
Kernel Solution:
- Package Manifest: The
manifestblock inobjectstack.config.ts(compiled toobjectstack.json) declares what a package provides and needs - Dependency Resolution: Automatic validation that plugin versions are compatible with core platform
- Lifecycle: every plugin implements a required
inithook plus optionalstart/destroyhooks, driven bykernel.bootstrap()and shutdown - Sandboxing: Plugins can't access each other's data or crash each other
Results:
- Launched marketplace with 50 third-party plugins in 6 months
- Zero "this plugin broke my system" incidents (validation prevents bad installs)
- 30% revenue growth from plugin marketplace sales
Global Enterprise Rollout
Challenge: A company expands from US to 12 countries (Europe, APAC, LATAM). Same app must support:
- 15 languages
- Different date/number formats (MM/DD/YYYY vs DD/MM/YYYY)
- Regional compliance (GDPR in EU, LGPD in Brazil)
- Local integrations (EU uses SEPA payments, US uses ACH)
Kernel Solution:
- i18n Standard: Translation bundles with fallback chains (
de-AT→de→en) - Region Config: Configuration profiles per region (defaults + region overrides)
- Plugin System: Regional plugins (SEPA plugin for EU, ACH plugin for US)
- Locale Resolution: Automatic detection from the user's localization settings or the request's
Accept-Languageheader, with explicit selection as an override
Results:
- Launched in 12 countries in 4 months (previously 18-month estimate)
- Zero code changes for new languages (just upload translation files)
- 95% translation coverage on day one (tooling validates translation completeness)
How Kernel Orchestrates ObjectQL and ObjectUI
Kernel is the runtime coordinator that makes ObjectQL and ObjectUI work together:
1. Boot Sequence
// Manual boot — the pattern the Runtime wrapper in @objectstack/runtime automates
import { ObjectKernel } from '@objectstack/core';
import { AppPlugin, DriverPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import stack from './objectstack.config';
const kernel = new ObjectKernel();
// Phase 1 — driver (storage backend)
await kernel.use(new DriverPlugin(new SqlDriver({ /* ... */ }), 'default'));
// Phase 2 — ObjectQL data layer
await kernel.use(new ObjectQLPlugin());
// Phase 3 — application metadata (objects, views, apps, flows, agents…)
await kernel.use(new AppPlugin(stack));
// Phase 4 — host (HTTP via HonoServerPlugin, MCP via MCPServerPlugin; optional)
// await kernel.use(new HonoServerPlugin({ port: 3000 }));
// Phase 5 — start everything
await kernel.bootstrap();The kernel validates each plugin's structure and version compatibility at
use(), then bootstrap() topologically sorts the registry — each plugin's
declared dependencies are hoisted ahead of it, registration order breaking
ties — and runs every plugin's init (kernel Phase 1) followed by every
plugin's start (kernel Phase 2), exposing the resulting services through DI.
It then fires kernel:ready, kernel:bootstrapped, and kernel:listening in
that order.
2. Request Handling
// Incoming API request: GET /api/v1/data/account/123
//
// The transport plugin (HonoServerPlugin) hands the raw request to the REST
// layer, which resolves ONE identity envelope and threads it into the engine:
// 1. Identity — resolveExecutionContext() reads the better-auth session (or
// API key), aggregates positions/permission sets/RLS membership, and layers
// locale + timezone on top. It always resolves; anonymous yields
// `{ isSystem: false, positions: [], permissions: [] }`.
const context = await resolveExecutionContext({ getService, getQl, request });
// → ExecutionContext { userId, tenantId, locale, timezone, positions,
// permissions, isSystem, ... }
// 2. Data — the context rides along as `context`, on the query itself or on
// the engine's trailing options argument (the engine merges both). It is
// what permissions and RLS are enforced against.
const ql = ctx.getService<any>('objectql');
const account = await ql.findOne('account', { where: { id: '123' }, context });Auditing is not called by hand at the request boundary: AuditPlugin subscribes
to the engine's data hooks and writes audit records as a side effect of the
operation.
3. Package Installation
// Install a package: os package install com.vendor.salesforce-sync
// (the argument is a reverse-domain manifest id, or a path to a compiled
// artifact JSON for an air-gapped install)
//
// A package is METADATA — no package code executes at install time.
// What actually happens:
//
// 1. Validate — manifest shape, engines.protocol compatibility (ADR-0025 §3.2),
// artifact signature.
// 2. Register — registry.installPackage() gates the namespace and records
// the InstalledPackage; the durable copy lands in `sys_packages` and is
// rehydrated on every boot.
// 3. Materialize — metadata is hot-registered, `syncSchemas` applies the
// schema, and declared side effects (translations, seed data) run.
//
// Executable members of an app bundle (`onEnable`, `functions`) run at BOOT,
// invoked by AppPlugin — installation only stores them.Philosophy: Infrastructure as Code
Kernel embodies "Infrastructure as Code" principles:
Declarative, Not Imperative
Bad (Imperative):
// Manual steps
db.createTable('accounts');
db.addColumn('accounts', 'name', 'string');
permissions.grant('admin', 'accounts', 'read');
// Miss a step? System broken.Good (Declarative):
# objectstack.config.ts — declared via defineStack({ ... })
# `objects` is a list; each object's `fields` is a MAP keyed by snake_case name
objects:
- name: account
fields:
name:
type: text
# PermissionSet grants use allow* flags (not role/object/access)
permissions:
- name: account_read
objects:
account:
allowRead: true
# Kernel ensures runtime matches the declared metadataIdempotent Operations
Run os package publish 100 times → Same result.
- Installing a plugin twice does nothing (it's already installed)
- Applying same configuration twice doesn't duplicate settings
- Schema migrations are versioned (run once, skip if already applied)
Version Control Everything
All system configuration lives in Git:
- Stack definition & manifest:
objectstack.config.ts(viadefineStack) - Compiled artifact:
dist/objectstack.json - Translation bundles:
i18n/en.json,i18n/de.json
Business Value: Rollback a bad deployment by reverting Git commit. No database state to recover, no manual cleanup.
Developer Experience
Plugin Development
# Scaffold new plugin (created under packages/plugins/plugin-<name>/)
os create plugin slack-integration
# Generated structure:
packages/plugins/plugin-slack-integration/
package.json # name, version, dependencies (@objectstack/spec, zod)
tsconfig.json
src/
index.ts # default-export Plugin object (name, version, initialize, destroy)
README.mdThe scaffold still emits an initialize method. The kernel's plugin contract
only invokes init / start / destroy, and init is required — rename
initialize to init in the generated src/index.ts or kernel.use()
rejects the plugin outright with
Failed to load plugin: slack-integration - Plugin init function is required.
Configuration Management
// A plugin object may carry a Zod `configSchema` describing its settings.
// NOTE: the kernel RECORDS the schema but does not enforce it yet — `use()`
// takes no config argument, so the loader has nothing to parse and logs
// "config validation postponed" instead of running the schema. Treat
// `configSchema` as a declaration of shape, and validate values you actually
// depend on yourself.
export const slackPlugin: Plugin = {
name: 'slack-integration',
version: '0.1.0',
configSchema: z.object({
apiKey: z.string().describe('Slack API Key'),
channel: z.string().default('#general'),
enabled: z.boolean().default(true),
}),
async init(ctx) {
// Runtime-resolved values come from the `settings` service, not from a
// `config` object on the context — there is no `ctx.config`.
const settings = ctx.getService<any>('settings');
const { value: channel } = await settings.get('slack', 'channel', {});
},
};Internationalization
// Define translations
// i18n/en.json
{
"slack.button.send": "Send to Slack",
"slack.error.invalid_channel": "Channel {{channel}} not found"
}
// i18n/de.json
{
"slack.button.send": "An Slack senden",
"slack.error.invalid_channel": "Kanal {{channel}} nicht gefunden"
}
// Use in code — the i18n service is resolved from the registry, and the
// locale is an explicit argument (take it from the ExecutionContext).
const i18n = ctx.getService<II18nService>('i18n');
const message = i18n.t('slack.button.send', context.locale);
// t(key, locale, params?) interpolates {{params}} and falls back to the
// configured fallback locale, returning the key itself when nothing matches.Best Practices
1. Embrace the Microkernel Philosophy
Principle: Core runtime does almost nothing. All features are plugins.
Why: Keeps core stable. Plugins evolve independently. Customers pay for only what they use.
Example: Don't build email sending into Kernel core. Ship it as the @objectstack/plugin-email package. Customers who don't send emails don't load the plugin (smaller memory footprint, faster boot).
2. Configuration Over Code
Principle: Behavior should be configurable without code changes.
Why: Reduces deployment frequency. Business users can toggle features without engineering.
Example: Instead of hardcoding maxRetries: 3 in plugin code, define maxRetries in config schema. Customers can override to 5 without touching code.
3. Design for Multi-Tenancy from Day One
Principle: Every piece of state (config, data, UI) should be tenant-scoped.
Why: Easier to add multi-tenancy later = rebuild entire system.
Example: Don't store config in global variables. Read through the settings service — settings.get(namespace, key, { tenantId, userId }) walks the global → tenant → user cascade for you.
4. Version Everything
Principle: All artifacts (plugins, schemas, configs) have semantic versions.
Why: Enables safe upgrades, rollbacks, and dependency management.
Example: Plugin manifest declares version: 2.1.0. Breaking change? Bump to 3.0.0. Consumers can pin to ^2.0.0 until they're ready to upgrade.
5. Fail Fast, Fail Loud
Principle: Invalid configuration should fail at boot time, not runtime.
Why: Catch errors before customers see them.
Example: Wiring, not config, is what the kernel currently enforces at boot. A plugin that declares a dependency the kernel never received fails bootstrap() outright — [Kernel] Dependency 'com.objectstack.engine.objectql' not found for plugin 'com.objectstack.audit' — and a dependency cycle throws [Kernel] Circular dependency detected: <plugin>. Boot stops there instead of a request failing later.
Plugin configSchema is not part of this fail-fast path yet. The loader
stores the schema and postpones the check, so a missing or malformed value in
a plugin's config will not stop boot today. Validate config you depend on in
your own init.
Comparison: Kernel vs Alternatives
| Feature | Kernel | Kubernetes | CloudFoundry | Heroku |
|---|---|---|---|---|
| Plugin System | Built-in manifest-based | Helm charts (external) | Buildpacks | Add-ons marketplace |
| Config Management | Hierarchical merge | ConfigMaps/Secrets | Environment vars | Config vars |
| Multi-Tenancy | Native tenant isolation | Namespace per tenant | Not supported | One app = one tenant |
| i18n | Built-in engine | Manual (libraries) | Manual | Manual |
| Dependency Resolution | Semantic versioning | Manual (YAML) | Manual | Manual |
| Zero-Downtime Upgrades | Declarative | Rolling updates | Blue-green | Git push |
| Developer UX | os package publish | kubectl apply | cf push | git push heroku |
Key Differentiator: Kernel is application-aware. Kubernetes knows about containers, not plugins. Kernel knows about ObjectQL objects, ObjectUI views, and business logic dependencies.
Next Steps
Learn Lifecycle Management
Understand boot sequence, installation, upgrade strategies, and rollback procedures
Build Your First Plugin
Create a plugin manifest, define dependencies, and implement lifecycle hooks
Master Configuration
Learn merge strategies, environment overrides, and tenant-specific settings
Internationalize Your App
Add multi-language support with translation bundles and locale resolution
Summary
Kernel is the control plane that orchestrates ObjectStack:
- Lifecycle Management: Declarative deployment, zero-downtime upgrades, rollback safety
- Plugin System: Microkernel architecture with dependency resolution and trust tiers
- Configuration: Unified config with merge strategies and tenant isolation
- i18n: Multi-language support built into the platform
Think of it this way:
- ObjectQL is the database driver (handles data CRUD)
- ObjectUI is the rendering engine (handles presentation)
- Kernel is the operating system (handles coordination, configuration, lifecycle)
Without Kernel, you'd manually wire ObjectQL and ObjectUI together, manage deployments with runbooks, and fight configuration drift. With Kernel, the platform handles the "plumbing" so you focus on business logic.