Metadata Customization
Metadata Customization protocol schemas
Metadata Customization Layer Protocol
Defines the overlay system for managing user customizations on top of
package-delivered metadata. This protocol solves the critical challenge
of separating "vendor-managed" metadata from "customer-customized" metadata,
enabling safe package upgrades without losing user changes.
Architecture Alignment
-
Salesforce: Managed vs Unmanaged metadata components
-
ServiceNow: Update Sets with collision detection
-
WordPress: Parent/child theme overlay model
-
Kubernetes: Strategic merge patch for resource customization
Three-Layer Model
┌─────────────────────────────────┐
│ User Layer (scope: user) │ ← Personal overrides (per-user)
├─────────────────────────────────┤
│ Platform Layer (scope: platform)│ ← Admin customizations (per-tenant)
├─────────────────────────────────┤
│ System Layer (scope: system) │ ← Package-delivered metadata (read-only)
└─────────────────────────────────┘Merge Resolution Order
Effective metadata = System ← merge(Platform) ← merge(User)
Each layer only stores the delta (changed fields), not the full definition.
Source: packages/spec/src/kernel/metadata-customization.zod.ts
TypeScript Usage
import { CustomizationOrigin, CustomizationPolicy, FieldChange, MergeConflict, MergeResult, MergeStrategyConfig, MetadataOverlay } from '@objectstack/spec/kernel';
import type { CustomizationOrigin, CustomizationPolicy, FieldChange, MergeConflict, MergeResult, MergeStrategyConfig, MetadataOverlay } from '@objectstack/spec/kernel';
// Validate data
const result = CustomizationOrigin.parse(data);CustomizationOrigin
Allowed Values
packageadminusermigrationapi
CustomizationPolicy
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| metadataType | string | ✅ | Metadata type (e.g. "object", "view") |
| allowCustomization | boolean | ✅ | |
| lockedFields | string[] | optional | Field paths that cannot be customized |
| customizableFields | string[] | optional | Field paths that can be customized (whitelist) |
| allowAddFields | boolean | ✅ | Whether admins can add new fields to package objects |
| allowDeleteFields | boolean | ✅ | Whether admins can delete package-delivered fields |
FieldChange
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | JSON path to the changed field |
| originalValue | any | optional | Original value from the package |
| currentValue | any | ✅ | Current customized value |
| changedBy | string | optional | User or admin who made this change |
| changedAt | string | optional | Timestamp of the change |
MergeConflict
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | JSON path to the conflicting field |
| baseValue | any | ✅ | Value in the old package version |
| incomingValue | any | ✅ | Value in the new package version |
| customValue | any | ✅ | Customer customized value |
| suggestedResolution | Enum<'keep-custom' | 'accept-incoming' | 'manual'> | ✅ | Suggested resolution strategy |
| reason | string | optional | Explanation for the suggested resolution |
MergeResult
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether merge completed without unresolved conflicts |
| mergedMetadata | Record<string, any> | optional | Merged metadata result |
| updatedOverlay | Record<string, any> | optional | Updated overlay after merge |
| conflicts | Object[] | optional | Unresolved merge conflicts |
| autoResolved | Object[] | optional | Summary of auto-resolved changes |
| stats | Object | optional |
MergeStrategyConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| defaultStrategy | Enum<'keep-custom' | 'accept-incoming' | 'three-way-merge'> | ✅ | Default merge strategy |
| alwaysAcceptIncoming | string[] | optional | Field paths that always accept package updates |
| alwaysKeepCustom | string[] | optional | Field paths where customer customizations always win |
| autoResolveNonConflicting | boolean | ✅ | Auto-resolve changes that do not conflict |
MetadataOverlay
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Overlay record ID (UUID) |
| baseType | string | ✅ | Metadata type being customized |
| baseName | string | ✅ | Metadata name being customized |
| packageId | string | optional | Package ID that delivered the base metadata |
| packageVersion | string | optional | Package version when overlay was created |
| scope | Enum<'platform' | 'user'> | ✅ | Customization scope (platform=admin, user=personal) |
| tenantId | string | optional | Tenant identifier |
| owner | string | optional | Owner user ID for user-scope overlays |
| patch | Record<string, any> | ✅ | JSON Merge Patch payload (changed fields only) |
| changes | Object[] | optional | Field-level change tracking for conflict detection |
| active | boolean | ✅ | Whether this overlay is active |
| createdAt | string | optional | |
| createdBy | string | optional | |
| updatedAt | string | optional | |
| updatedBy | string | optional |