Metadata Customization Metadata Customization protocol schemas
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.
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
┌─────────────────────────────────┐
│ 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)
└─────────────────────────────────┘
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
import { CustomizationOriginSchema, CustomizationPolicySchema, FieldChangeSchema, MergeConflictSchema, MergeResultSchema, MergeStrategyConfigSchema, MetadataOverlaySchema } from '@objectstack/spec/kernel' ;
import type { CustomizationOrigin, CustomizationPolicy, FieldChange, MergeConflict, MergeResult, MergeStrategyConfig, MetadataOverlay } from '@objectstack/spec/kernel' ;
// Validate data
const result = CustomizationOriginSchema. parse (data);
package
admin
user
migration
api
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
Property Type Required Description path string✅ JSON path to the changed field originalValue anyoptional Original value from the package currentValue any✅ Current customized value changedBy stringoptional User or admin who made this change changedAt stringoptional Timestamp of the change
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 stringoptional Explanation for the suggested resolution
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 { path: string; baseValue: any; incomingValue: any; customValue: any; … }[]optional Unresolved merge conflicts autoResolved { path: string; resolution: string; description?: string }[]optional Summary of auto-resolved changes stats { totalFields: integer; unchanged: integer; autoResolved: integer; conflicts: integer }optional
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
Property Type Required Description id string✅ Overlay record ID (UUID) baseType string✅ Metadata type being customized baseName string✅ Metadata name being customized packageId stringoptional Package ID that delivered the base metadata packageVersion stringoptional Package version when overlay was created scope Enum<'platform' | 'user'>✅ Customization scope (platform=admin, user=personal) tenantId stringoptional Tenant identifier owner stringoptional Owner user ID for user-scope overlays patch Record<string, any>✅ JSON Merge Patch payload (changed fields only) changes { path: string; originalValue?: any; currentValue: any; changedBy?: string; … }[]optional Field-level change tracking for conflict detection active boolean✅ Whether this overlay is active createdAt stringoptional createdBy stringoptional updatedAt stringoptional updatedBy stringoptional