---
title: IMetadataService Contract
description: Reference for the Metadata Service contract — CRUD operations for object and field definitions, schema registry, overlay management, and import/export
---

# IMetadataService Contract

The Metadata Service manages all object and field definitions at runtime. It serves as the **schema registry** — plugins, the Kernel, and the API layer all query this service to discover what objects exist and what fields they contain.

<Callout type="info">
**Source:** `packages/spec/src/system/contracts/metadata-service.ts`  
**Token:** `METADATA_SERVICE`
</Callout>

<Callout type="tip">
For the **data path** behind this contract — Repository → Change Log → Cache → Registry, plus HMR semantics — see [Metadata Lifecycle & HMR](/concepts/metadata-lifecycle).
</Callout>

---

## Interface Definition

```typescript
export interface IMetadataService {
  // Object CRUD
  getObject(name: string): Promise<ObjectDefinition | null>;
  listObjects(filter?: MetadataFilter): Promise<ObjectDefinition[]>;
  createObject(definition: ObjectDefinition): Promise<ObjectDefinition>;
  updateObject(name: string, changes: Partial<ObjectDefinition>): Promise<ObjectDefinition>;
  deleteObject(name: string): Promise<void>;

  // Field CRUD
  getField(objectName: string, fieldName: string): Promise<FieldDefinition | null>;
  listFields(objectName: string): Promise<FieldDefinition[]>;
  createField(objectName: string, field: FieldDefinition): Promise<FieldDefinition>;
  updateField(objectName: string, fieldName: string, changes: Partial<FieldDefinition>): Promise<FieldDefinition>;
  deleteField(objectName: string, fieldName: string): Promise<void>;

  // Bulk Operations
  bulkUpsert(definitions: ObjectDefinition[]): Promise<BulkResult>;
  validateDefinitions(definitions: ObjectDefinition[]): Promise<ValidationResult>;

  // Query
  query(filter: MetadataFilter): Promise<MetadataQueryResult>;
  getRelationships(objectName: string): Promise<Relationship[]>;
  getDependencies(objectName: string): Promise<DependencyGraph>;

  // Overlay Management
  applyOverlay(overlay: MetadataOverlay): Promise<void>;
  getEffectiveDefinition(objectName: string): Promise<ObjectDefinition>;
  listOverlays(objectName?: string): Promise<MetadataOverlay[]>;

  // Watch / Subscribe
  watch(pattern: string, callback: MetadataChangeCallback): Unsubscribe;
  onChange(callback: MetadataChangeCallback): Unsubscribe;

  // Import / Export
  exportAll(): Promise<MetadataPackage>;
  importPackage(pkg: MetadataPackage, options?: ImportOptions): Promise<ImportResult>;
}
```

---

## Object CRUD

### getObject

Retrieves a single object definition by its machine name.

```typescript
const task = await metadataService.getObject('task');
if (task) {
  console.log(task.label);        // "Task"
  console.log(task.fields.length); // 12
}
```

### listObjects

Returns all object definitions, optionally filtered.

```typescript
// All objects
const allObjects = await metadataService.listObjects();

// Only API-enabled objects
const apiObjects = await metadataService.listObjects({
  where: { 'enable.apiEnabled': true }
});
```

### createObject

Registers a new object definition. Validates the schema before persisting.

```typescript
const newObject = await metadataService.createObject({
  name: 'project',
  label: 'Project',
  pluralLabel: 'Projects',
  fields: [
    { name: 'name', label: 'Name', type: 'text', required: true },
    { name: 'status', label: 'Status', type: 'select', options: [
      { label: 'Active', value: 'active' },
      { label: 'Archived', value: 'archived' },
    ]},
  ],
  enable: { apiEnabled: true, trackHistory: true },
});
```

### updateObject

Applies partial changes to an existing object definition.

```typescript
await metadataService.updateObject('project', {
  label: 'Project Workspace',
  enable: { search: true },
});
```

### deleteObject

Removes an object definition. Fails if other objects reference it.

```typescript
await metadataService.deleteObject('project');
```

<Callout type="tip">
**Cascade Check:** `deleteObject` checks for lookup and master_detail references. Use `getDependencies()` first to inspect what would break.
</Callout>

---

## Field CRUD

### getField / listFields

```typescript
const statusField = await metadataService.getField('task', 'status');
const allFields = await metadataService.listFields('task');
```

### createField / updateField / deleteField

```typescript
// Add a field
await metadataService.createField('task', {
  name: 'estimated_hours',
  label: 'Estimated Hours',
  type: 'number',
  min: 0,
  max: 1000,
});

// Rename field label
await metadataService.updateField('task', 'estimated_hours', {
  label: 'Effort (Hours)',
});

// Remove a field
await metadataService.deleteField('task', 'estimated_hours');
```

---

## Bulk Operations

### bulkUpsert

Creates or updates multiple object definitions in a single transaction.

```typescript
const result = await metadataService.bulkUpsert([
  { name: 'task', label: 'Task', fields: [/* ... */] },
  { name: 'project', label: 'Project', fields: [/* ... */] },
]);

console.log(result.created);  // 1
console.log(result.updated);  // 1
console.log(result.errors);   // []
```

### validateDefinitions

Validates definitions without persisting. Useful for dry-run imports.

```typescript
const validation = await metadataService.validateDefinitions(definitions);
if (!validation.valid) {
  console.error(validation.errors);
}
```

---

## Overlay Management

Overlays allow runtime customization of object definitions without modifying the base definition.

```typescript
// Apply an overlay that adds a custom field for a specific tenant
await metadataService.applyOverlay({
  target: 'task',
  source: 'tenant:acme_corp',
  priority: 10,
  changes: {
    fields: {
      add: [{ name: 'custom_score', label: 'Score', type: 'number' }],
    },
  },
});

// Get the merged definition (base + overlays)
const effective = await metadataService.getEffectiveDefinition('task');
```

| Property | Type | Description |
|:---|:---|:---|
| `target` | `string` | Object name the overlay applies to |
| `source` | `string` | Identifier for the overlay origin |
| `priority` | `number` | Higher priority overlays win on conflicts |
| `changes` | `object` | Field additions, modifications, removals |

---

## Watch / Subscribe

Subscribe to metadata changes for live-reload and cache invalidation.

```typescript
// Watch all changes
const unsubscribe = metadataService.onChange((event) => {
  console.log(`${event.type} on ${event.objectName}`);
  // "fieldCreated on task"
});

// Watch specific object pattern
const unsub = metadataService.watch('task.*', (event) => {
  invalidateCache('task');
});

// Clean up
unsubscribe();
```

---

## Import / Export

### exportAll

Exports the complete metadata catalog as a portable package.

```typescript
const pkg = await metadataService.exportAll();
// pkg.objects, pkg.version, pkg.exportedAt
```

### importPackage

Imports a metadata package with conflict resolution options.

```typescript
const result = await metadataService.importPackage(pkg, {
  onConflict: 'merge',      // 'merge' | 'overwrite' | 'skip'
  dryRun: false,
  validateOnly: false,
});

console.log(result.created);    // 3
console.log(result.updated);    // 2
console.log(result.skipped);    // 1
console.log(result.errors);     // []
```

---

## Types Reference

| Type | Description |
|:---|:---|
| `ObjectDefinition` | Full object schema (name, label, fields, enable) |
| `FieldDefinition` | Single field schema (name, label, type, options) |
| `MetadataFilter` | Query filter for listing objects/fields |
| `MetadataOverlay` | Runtime customization layer |
| `MetadataPackage` | Portable metadata export format |
| `MetadataChangeCallback` | `(event: MetadataChangeEvent) => void` |
| `Unsubscribe` | `() => void` — call to stop watching |
| `BulkResult` | `{ created, updated, errors }` |
| `ImportResult` | `{ created, updated, skipped, errors }` |

---

## UI Metadata (Views & Dashboards)

UI metadata types (`view`, `dashboard`, `page`, `app`, `theme`) are first-class citizens in the Metadata Service. The previously separate `IUIService` has been deprecated.

### Reading UI Metadata

```typescript
// Get a view definition
const view = await metadataService.get('view', 'account_list');

// List all views for an object
const views = await metadataService.listViews('account');

// Get a dashboard
const dashboard = await metadataService.get('dashboard', 'sales_overview');
```

### User-Level Customization

Users can customize views via the overlay system:

```typescript
// Admin customizes a view for all users
await metadataService.saveOverlay({
  id: 'overlay-platform-1',
  baseType: 'view',
  baseName: 'account_list',
  scope: 'platform',
  patch: { columns: ['name', 'email', 'status', 'created_at'] },
});

// A specific user saves personal column preferences
await metadataService.saveOverlay({
  id: 'overlay-user-123',
  baseType: 'view',
  baseName: 'account_list',
  scope: 'user',
  owner: 'user-123',
  patch: { columns: ['name', 'status'] },  // user only wants 2 columns
});

// Resolve effective view for a specific user
const effectiveView = await metadataService.getEffective('view', 'account_list', {
  userId: 'user-123',
});
// Result: base view ← platform overlay ← user-123 overlay
```

### Permission-Based UI Filtering

Permission filtering is handled at the API layer, not in the metadata service itself:

```typescript
// In your API handler / middleware:
const views = await metadataService.list('view');
const filteredViews = views.filter(view => {
  const v = view as any;
  // Check if user has permission to see this view
  return !v.requiredPermission || userPermissions.includes(v.requiredPermission);
});
```

---

## Package Publishing

ObjectStack uses **package-level publishing** to ensure metadata consistency. All metadata items within a package are published atomically — either everything goes live, or nothing does.

### publishPackage

Publishes all metadata items in a package:
1. Validates all items (optional)
2. Snapshots each item's definition into `publishedDefinition`
3. Increments the package version
4. Sets all items to `active` state

```typescript
const result = await metadataService.publishPackage('com.acme.crm', {
  publishedBy: 'admin-user',
  validate: true,        // default: true
  changeNote: 'Added opportunity fields',
});

console.log(result.success);         // true
console.log(result.version);         // 2
console.log(result.itemsPublished);  // 5
console.log(result.publishedAt);     // "2025-06-01T12:00:00Z"
```

### revertPackage

Reverts all metadata items in a package to their last published state. Discards any unpublished changes.

```typescript
await metadataService.revertPackage('com.acme.crm');
// All items restored to their publishedDefinition snapshots
```

### getPublished

Returns the published version of a metadata item (for runtime/end-user serving). Falls back to the current definition if the item has never been published.

```typescript
// End user sees the published version
const published = await metadataService.getPublished('object', 'opportunity');

// Designer sees the draft version (via regular get)
const draft = await metadataService.get('object', 'opportunity');
```

### REST Endpoints

| Method | Path | Description |
|:---|:---|:---|
| `POST` | `/packages/:id/publish` | Publish a package |
| `POST` | `/packages/:id/revert` | Revert a package to last published state |
| `GET` | `/metadata/:type/:name/published` | Get published version of a metadata item |
