Metadata Service
The unified engine for loading, saving, and managing configuration metadata across the platform
The Metadata Service is the backbone of ObjectStack configuration management. It decouples the definition of metadata (Objects, Views, Flows) from its runtime delivery path: local development reads files or dist/objectstack.json, while production ObjectStack reads an immutable artifact from the control plane. Project databases store business rows only.
Architecture
The Metadata Service uses a Provider/Loader architecture. The core MetadataManager acts as a registry and orchestrator, delegating actual I/O operations to registered Loaders.
Registry Pattern
The Manager accepts any number of Loaders. Runtime ObjectStack uses file/artifact loaders; control-plane services can opt into database loaders explicitly.
Format Agnostic
Automatically handles JSON, YAML, and TypeScript formats. Developers write TS, Machines write JSON.
Hot Reload
Built-in file watching. Edit a file in VS Code, and the runtime updates immediately.
Atomic Persistence
Safe save API with backup generation and atomic writes to prevent data corruption.
Core Concepts
1. The Manager
The MetadataManager is the single entry point for all metadata operations. It handles caching, serialization, and correct routing of requests to loaders.
import { NodeMetadataManager } from '@objectstack/metadata/node';
// NodeMetadataManager wires up the FilesystemLoader and file watcher.
// The browser-safe base `MetadataManager` (from '@objectstack/metadata')
// registers no loaders on its own — callers must provide them.
const manager = new NodeMetadataManager({
rootDir: './src',
watch: true // Enable hot reload
});
// Load an Object Definition
const accountObj = await manager.load('object', 'account');2. Loaders (Data Sources)
Loaders adapt different storage backends to a common interface.
| Loader | Type | Description |
|---|---|---|
| FilesystemLoader | filesystem | Reads/Writes .json, .yaml, .ts files from disk. Primary for development. |
| MemoryLoader | memory | Hydrates compiled artifact items into the runtime metadata registry. |
| DatabaseLoader | database | Stores metadata revisions in control-plane services that explicitly configure it. Not auto-enabled by ObjectStack. |
| RemoteLoader | http | Fetches metadata from a remote HTTP API. |
3. Granular Capabilities
Each Loader declares its capabilities via a Contract:
export interface MetadataLoaderContract {
name: string;
protocol: 'file:' | 'http:' | 's3:' | 'datasource:' | 'memory:';
capabilities: {
read: boolean; // Can read items?
write: boolean; // Can save modifications?
watch: boolean; // Can push real-time updates?
list: boolean; // Can enumerate items?
};
supportedFormats: ('json' | 'yaml' | 'typescript' | 'javascript')[];
}Internal Persistence API
The Metadata Service supports full CRUD operations for authoring tools and control-plane services. Runtime ObjectStack treats metadata as read-only after artifact/file hydration; MetadataPlugin does not automatically bridge ObjectQL to DatabaseLoader.
Saving Metadata (Node.js)
// 1. Simple Save (Auto-detects loader)
await manager.save('object', 'customer_profile', data);
// 2. Targeted Save (Specific Source)
await manager.save('object', 'customer_profile', data, {
loader: 'filesystem',
format: 'yaml', // Convert to YAML on save
atomic: true // Use write-rename strategy for safety
});
// 3. Create Backup
await manager.save('object', 'workflow_config', data, {
backup: true // Creates 'workflow_config.json.bak' before writing
});Loading Strategy
When load() is called, the Manager iterates through registered loaders. In ObjectStack runtime boot this means "artifact/file first"; database-backed metadata is reserved for explicitly configured control-plane services.
graph LR
A[Request: load('object', 'user')] --> B{Metadata Manager}
B --> C[Check Artifact / Memory Loader]
C -- Not Found --> D[Check Filesystem Loader]
D -- Found --> E[Return & Cache]
C -- Found --> E
Watching & Hot Reload
The Service emits events when metadata changes, allowing the runtime to reconfigure itself without restarting.
const unsubscribe = manager.subscribe('object', (event) => {
console.log(`Object ${event.name} changed!`);
console.log(`New Schema:`, event.data);
// Trigger system update
SchemaEngine.refresh(event.name);
});Remote API Access
ObjectStack metadata is artifact-backed at runtime. Read/write metadata APIs belong to authoring tools and the control plane.
The system provides standard REST and TypeScript APIs for interacting with metadata, including full read/write support.
REST API
GET /api/v1/meta: List all metadata types.GET /api/v1/meta/:type: List items of a type.GET /api/v1/meta/:type/:name: Get the definition of an item.PUT /api/v1/meta/:type/:name: Save or update a metadata item.
Client SDK
// Save metadata from the browser
const result = await client.meta.saveItem('object', 'customer', {
label: 'Customer',
fields: {
name: { type: 'text', required: true }
}
});Server-Side Protocol
// Access from Protocol Service (Server-side)
const protocol = kernel.getService('protocol');
await protocol.saveMetaItem({
type: 'object',
name: 'lead',
item: { ... }
});Runtime Controls (v3.1+)
Four orthogonal levers shape how MetadataManager and MetadataPlugin behave at boot and at request time. All four default to "dev-friendly" so existing code keeps working.
1. Bootstrap Modes
MetadataPluginConfigSchema.bootstrap decides what the plugin does on start():
| Mode | When to use | Behavior |
|---|---|---|
eager (default) | Local dev, traditional servers | Scans filesystem (or hydrates from artifactSource) at boot. |
lazy | Cold-start sensitive runtimes, on-demand workloads | Skips the FS prime; reads flow through registered loaders (incl. DatabaseLoader cache). Honors artifactSource when set. |
artifact-only | Edge / serverless / read-only production | Refuses to touch the filesystem. Requires artifactSource (mode: 'local-file'; path may be an http(s) URL). |
new MetadataPlugin({
config: { bootstrap: 'artifact-only' },
artifactSource: { mode: 'local-file', path: './dist/objectstack.json' },
});Artifact source
artifactSource: { mode: 'local-file', path, fetchTimeoutMs? } is the single
artifact source, honored by every bootstrap mode above. path is a filesystem
path or an http(s) URL fetched verbatim — the control plane's public
artifact route (/pub/v1/environments/:id/artifact[?commit=<id>]) serves
exactly such URLs, so a sealed runtime can boot straight off a published
revision:
new MetadataPlugin({
config: { bootstrap: 'artifact-only' },
artifactSource: {
mode: 'local-file',
path: 'https://cloud.example.com/pub/v1/environments/env_42/artifact?commit=cmt_1a2b',
},
});Only non-URL paths get the artifact-file HMR watcher (artifactWatch); remote
reads honor fetchTimeoutMs (and OS_ARTIFACT_FETCH_TIMEOUT_MS, default
60 s). Outside artifact-only, an absent local file is "nothing compiled
yet", not a fault.
A second artifact-api mode (a Bearer-authenticated control-plane pull) was
removed while resolving #4246: it had no consumers — the cloud runtime uses
its own ArtifactApiClient, and package distribution into a running OSS
instance goes through @objectstack/cloud-connection (os package install).
A still-configured artifact-api source fails loudly at start() rather
than silently falling back to the filesystem scan.
2. Persistence Write Gates
MetadataManagerConfigSchema.persistence is a two-axis runtime freeze. Both flags default to true.
| Flag | Effect when false |
|---|---|
persistence.writable | register() becomes a no-op (or throws when validation.throwOnError). |
persistence.overlayWritable | saveOverlay() is rejected. Disables Studio overlays in sealed deployments. |
new MetadataManager({
persistence: { writable: false, overlayWritable: false },
});3. DatabaseLoader Read-Through Cache
DatabaseLoader wraps load / loadMany / list / stat results in a generic LRU cache (lazy TTL, promote-on-get, write invalidation). Reads always observe writes performed through the same loader instance; out-of-band SQL writes are honored within ttl milliseconds.
new MetadataManager({
datasource: 'default',
cache: {
enabled: true,
databaseLoader: { enabled: true, maxSize: 500, ttl: 60_000 },
},
});The cache exposes LRUCache.stats() (size / hits / misses / hitRate) for metrics.
4. Single-Source Schema Discipline
The canonical MetadataManagerConfigSchema and MetadataFallbackStrategySchema live in kernel/metadata-loader.zod.ts. The persistence-side module system/metadata-persistence.zod.ts re-exports them, so consumers observe a single TypeScript type regardless of import path. Drift between kernel-side runtime config and persistence-side typing is no longer possible.
CLI & Tooling Integration
The Metadata Service is designed to power the CLI.
os compile: Validates TypeScript metadata and emitsdist/objectstack.json.os package publish: Uploads the compileddist/objectstack.jsonartifact to the control plane as a versioned package.os dev: Boots ObjectStack from local files or the compiled artifact without a control-plane connection.