name: Deterministic field universal identifiers
overview: Make all fieldMetadata universal identifiers deterministic via getFieldUniversalIdentifier across the custom-object creation path, the twenty-standard app static definition, and the SDK, then backfill every existing fieldMetadata row (and stored app manifests) to the new derivation.
todos:
- id: custom-object-defaults
content: Switch build-default-flat-field-metadatas-for-custom-object and create-field-input default to getFieldUniversalIdentifier
status: pending
- id: standard-objects
content: Compute STANDARD_OBJECTS field UIDs via getFieldUniversalIdentifier at module load
status: pending
- id: sdk-alignment
content: Align SDK generateDefaultFieldUniversalIdentifier with getFieldUniversalIdentifier, thread app UID through manifest build
status: pending
- id: workspace-backfill
content: "2-19 workspace command: backfill all fieldMetadata.universalIdentifier via raw SQL + cache invalidation"
status: pending
- id: manifest-backfill
content: "2-19 slow instance command: rewrite field UIDs in stored application manifests"
status: pending
- id: tests-verification
content: Update tests/snapshots/seeds, run typecheck, lint, and integration verification
status: pending
isProject: false
Deterministic Field Universal Identifiers
Goal
One single derivation everywhere: getFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier, name }) (UUID v5, namespace = app UID, seed = fieldMetadata:{objectUID}:{name}) from packages/twenty-shared/src/application/deterministic-identifier/get-field-universal-identifier.util.ts. Existing rows are backfilled to match.
1. Custom object default fields (server create path)
In packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts:
- Replace all 9
universalIdentifier: v4() (8 system fields + nameField) with getFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier: objectMetadataUniversalIdentifier, name: <fieldName> }). Both inputs are already available in scope.
- Also update
get-default-flat-field-metadata-from-create-field-input.util.ts (createFieldInput.universalIdentifier ?? v4()) so user-created fields follow the same derivation when no UID is provided — otherwise the "all fields deterministic" invariant breaks the moment a user creates a field. Note: this assumes hard-deleted fields free their name; if a soft-deleted row can coexist with a same-name recreation we'd get a UID collision (verify during implementation).
2. Twenty-standard app static definition
In packages/twenty-shared/src/metadata/constants/standard-object.constant.ts (~900 field UIDs):
- Replace every hardcoded field
universalIdentifier literal with a computed call: getFieldUniversalIdentifier({ applicationUniversalIdentifier: TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: <object's UID>, name: '<fieldName>' }), evaluated at module load. Likely via a small local helper to keep the file readable, e.g. standardField(objectUniversalIdentifier, name).
- Object UIDs, morphIds, and index UIDs stay hardcoded (out of scope).
- Update the "never mutate" header comment to explain the new derivation contract (field UIDs are now derived; renaming a standard field name IS a UID mutation and must never happen without a coordinated backfill).
- All standard-app builders (
create-standard-field-flat-metadata.util.ts, create-standard-relation-field-flat-metadata.util.ts, view/index definitions) read from STANDARD_OBJECTS, so they pick up new values automatically. Check for any snapshot tests / seed data asserting hardcoded field UIDs and update them.
- Watch for import cycles:
standard-object.constant.ts (twenty-shared/metadata) will now import from twenty-shared/application.
3. SDK default system fields
- Rewrite packages/twenty-sdk/src/sdk/define/objects/generate-default-field-universal-identifier.ts to delegate to
getFieldUniversalIdentifier. New required input: applicationUniversalIdentifier — thread it through:
getDefaultObjectFields / getDefaultRelationObjectFields and their caller getDefaultFieldsInObjectFields in packages/twenty-sdk/src/cli/utilities/build/manifest/ (the manifest build knows the application config, so pass the app UID down from manifest-build.ts).
- The public export used by app authors (
entity-view-template.ts and @/sdk/define index) — signature change is breaking for app authors; update templates and tests/snapshots (get-default-object-fields.spec.ts, stub-twenty-sdk-define.plugin.spec.ts.snap, get-view-base-file.spec.ts).
- Keep the old derivation function (fixed namespace
142046f0-..., seed {objectUID}-{fieldName}) available server-side (or copied into the backfill command) as computeLegacySdkDefaultFieldUniversalIdentifier — needed for the backfill's old→new mapping in stored manifests.
4. Backfill commands (version 2.19)
universalIdentifier is excluded from the migration-runner updatable properties, so this is raw SQL + manual cache invalidation (same pattern as the 2-18 normalize-legacy-index-names command).
4a. Workspace command: backfill fieldMetadata rows
New file in packages/twenty-server/src/database/commands/upgrade-version-command/2-19/, registered in 2-19-upgrade-version-command.module.ts, extending ActiveOrSuspendedWorkspaceCommandRunner:
- For every
fieldMetadata row in the workspace (standard, workspace-custom, and installed-SDK-app fields alike): compute newUniversalIdentifier = getFieldUniversalIdentifier({ applicationUniversalIdentifier: <row's application.universalIdentifier>, objectUniversalIdentifier: <row's objectMetadata.universalIdentifier>, name: row.name }) and update via raw SQL where it differs. Idempotent by construction (already-deterministic rows are no-ops).
- Support
--dry-run and log per-workspace update counts.
- After update:
workspaceCacheService.invalidateAndRecompute(workspaceId, [...]) for flatFieldMetadataMaps plus related maps (getMetadataRelatedMetadataNames('fieldMetadata')), wrapped in try/catch so cache failure doesn't fail the upgrade.
- DB relations reference fields by
id, not UID, so the row update is self-contained — but verify no jsonb settings store field UIDs (e.g. universalSettings.junctionTargetFieldUniversalIdentifier is derived at cache-compute time, not stored).
- Ordering: must run before the standard-app / application sync during upgrade (it does, since version commands run before the final sync-metadata step) — confirm during implementation.
4b. Instance command (slow): rewrite stored app manifests
Installed SDK apps have applicationRegistration.manifest (jsonb) still containing old-derivation default-field UIDs (in fields[].universalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier, and view field references). Without this, the next app deploy/sync diffs against mismatched UIDs and deletes/recreates fields.
- Generate via
npx nx run twenty-server:database:migrate:generate --name <name> --type slow.
- For each registration manifest: for each object, compute the old SDK UID and the new UID for each default field name, and replace every occurrence throughout the manifest json (string-level old→new replacement is safe since UUIDs are globally unique). Also handle explicitly-authored (non-default) field UIDs? No — those were author-chosen and are backfilled workspace-side by 4a; but then manifest vs workspace diverges for those too. Decision baked in: 4a recomputes ALL fields including author-defined ones, so 4b must rewrite ALL field UIDs in manifests using the same derivation (new UID from app UID + object UID + field name), not just default fields.
- Check whether
application (workspace-scoped) also stores a manifest copy and rewrite it too if so.
5. Verification
- Unit tests: updated SDK specs and snapshots; add a spec asserting
buildDefaultFlatFieldMetadatasForCustomObject produces stable UIDs for a fixed input.
npx nx run twenty-server:typecheck, npx nx typecheck twenty-shared, SDK typecheck, lint:diff-with-main on touched packages.
- Integration: run existing metadata suites (object creation, application sync); run the backfill command against a seeded database and re-run
sync-metadata to confirm the standard sync produces no diff.
Risks / notes
- The
STANDARD_OBJECTS "never mutate" rule is deliberately violated once, in lockstep with the 4a backfill; any workspace upgraded without the command running would have its standard fields deleted/recreated by sync. The command registration in 2-19 guarantees ordering for the normal upgrade path.
- Renaming a field after creation keeps its UID (UID is derived at creation/backfill time only) — that's expected and consistent with UID-as-stable-identity.
- Any hardcoded field UIDs in tests, seeds (
prefill data), or dev fixtures will need regeneration — will grep for the old literal values.
name: Deterministic field universal identifiers
overview: Make all fieldMetadata universal identifiers deterministic via getFieldUniversalIdentifier across the custom-object creation path, the twenty-standard app static definition, and the SDK, then backfill every existing fieldMetadata row (and stored app manifests) to the new derivation.
todos:
content: Switch build-default-flat-field-metadatas-for-custom-object and create-field-input default to getFieldUniversalIdentifier
status: pending
content: Compute STANDARD_OBJECTS field UIDs via getFieldUniversalIdentifier at module load
status: pending
content: Align SDK generateDefaultFieldUniversalIdentifier with getFieldUniversalIdentifier, thread app UID through manifest build
status: pending
content: "2-19 workspace command: backfill all fieldMetadata.universalIdentifier via raw SQL + cache invalidation"
status: pending
content: "2-19 slow instance command: rewrite field UIDs in stored application manifests"
status: pending
content: Update tests/snapshots/seeds, run typecheck, lint, and integration verification
status: pending
isProject: false
Deterministic Field Universal Identifiers
Goal
One single derivation everywhere:
getFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier, name })(UUID v5, namespace = app UID, seed =fieldMetadata:{objectUID}:{name}) from packages/twenty-shared/src/application/deterministic-identifier/get-field-universal-identifier.util.ts. Existing rows are backfilled to match.1. Custom object default fields (server create path)
In packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts:
universalIdentifier: v4()(8 system fields +nameField) withgetFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier: objectMetadataUniversalIdentifier, name: <fieldName> }). Both inputs are already available in scope.get-default-flat-field-metadata-from-create-field-input.util.ts(createFieldInput.universalIdentifier ?? v4()) so user-created fields follow the same derivation when no UID is provided — otherwise the "all fields deterministic" invariant breaks the moment a user creates a field. Note: this assumes hard-deleted fields free their name; if a soft-deleted row can coexist with a same-name recreation we'd get a UID collision (verify during implementation).2. Twenty-standard app static definition
In packages/twenty-shared/src/metadata/constants/standard-object.constant.ts (~900 field UIDs):
universalIdentifierliteral with a computed call:getFieldUniversalIdentifier({ applicationUniversalIdentifier: TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: <object's UID>, name: '<fieldName>' }), evaluated at module load. Likely via a small local helper to keep the file readable, e.g.standardField(objectUniversalIdentifier, name).create-standard-field-flat-metadata.util.ts,create-standard-relation-field-flat-metadata.util.ts, view/index definitions) read fromSTANDARD_OBJECTS, so they pick up new values automatically. Check for any snapshot tests / seed data asserting hardcoded field UIDs and update them.standard-object.constant.ts(twenty-shared/metadata) will now import from twenty-shared/application.3. SDK default system fields
getFieldUniversalIdentifier. New required input:applicationUniversalIdentifier— thread it through:getDefaultObjectFields/getDefaultRelationObjectFieldsand their callergetDefaultFieldsInObjectFieldsinpackages/twenty-sdk/src/cli/utilities/build/manifest/(the manifest build knows the application config, so pass the app UID down frommanifest-build.ts).entity-view-template.tsand@/sdk/defineindex) — signature change is breaking for app authors; update templates and tests/snapshots (get-default-object-fields.spec.ts,stub-twenty-sdk-define.plugin.spec.ts.snap,get-view-base-file.spec.ts).142046f0-..., seed{objectUID}-{fieldName}) available server-side (or copied into the backfill command) ascomputeLegacySdkDefaultFieldUniversalIdentifier— needed for the backfill's old→new mapping in stored manifests.4. Backfill commands (version 2.19)
universalIdentifieris excluded from the migration-runner updatable properties, so this is raw SQL + manual cache invalidation (same pattern as the 2-18normalize-legacy-index-namescommand).4a. Workspace command: backfill fieldMetadata rows
New file in
packages/twenty-server/src/database/commands/upgrade-version-command/2-19/, registered in 2-19-upgrade-version-command.module.ts, extendingActiveOrSuspendedWorkspaceCommandRunner:fieldMetadatarow in the workspace (standard, workspace-custom, and installed-SDK-app fields alike): computenewUniversalIdentifier = getFieldUniversalIdentifier({ applicationUniversalIdentifier: <row's application.universalIdentifier>, objectUniversalIdentifier: <row's objectMetadata.universalIdentifier>, name: row.name })and update via raw SQL where it differs. Idempotent by construction (already-deterministic rows are no-ops).--dry-runand log per-workspace update counts.workspaceCacheService.invalidateAndRecompute(workspaceId, [...])forflatFieldMetadataMapsplus related maps (getMetadataRelatedMetadataNames('fieldMetadata')), wrapped in try/catch so cache failure doesn't fail the upgrade.id, not UID, so the row update is self-contained — but verify no jsonb settings store field UIDs (e.g.universalSettings.junctionTargetFieldUniversalIdentifieris derived at cache-compute time, not stored).4b. Instance command (slow): rewrite stored app manifests
Installed SDK apps have
applicationRegistration.manifest(jsonb) still containing old-derivation default-field UIDs (infields[].universalIdentifier,labelIdentifierFieldMetadataUniversalIdentifier, and view field references). Without this, the next app deploy/sync diffs against mismatched UIDs and deletes/recreates fields.npx nx run twenty-server:database:migrate:generate --name <name> --type slow.application(workspace-scoped) also stores a manifest copy and rewrite it too if so.5. Verification
buildDefaultFlatFieldMetadatasForCustomObjectproduces stable UIDs for a fixed input.npx nx run twenty-server:typecheck,npx nx typecheck twenty-shared, SDK typecheck,lint:diff-with-mainon touched packages.sync-metadatato confirm the standard sync produces no diff.Risks / notes
STANDARD_OBJECTS"never mutate" rule is deliberately violated once, in lockstep with the 4a backfill; any workspace upgraded without the command running would have its standard fields deleted/recreated by sync. The command registration in 2-19 guarantees ordering for the normal upgrade path.prefilldata), or dev fixtures will need regeneration — will grep for the old literal values.