diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260613022355_CleanupSystemPlatformNamespaces.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260613022355_CleanupSystemPlatformNamespaces.cs
index b1327863..a15163e7 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260613022355_CleanupSystemPlatformNamespaces.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260613022355_CleanupSystemPlatformNamespaces.cs
@@ -29,6 +29,15 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
/// is keyed off the set of SystemPlatform namespaces, so on a DB with zero such rows every
/// DELETE simply affects 0 rows (idempotent / safe to run repeatedly).
///
+ ///
+ /// Two tables carry LOGICAL refs with no physical FK and so were easy to miss: NodeAcl
+ /// (per-scope grants keyed by ScopeKind + ScopeId) is cleaned for the
+ /// Namespace/Equipment/FolderSegment/Tag scopes that get deleted, and
+ /// ExternalIdReservation (fleet-wide ZTag/SAPID reservations keyed by
+ /// EquipmentUuid) is RELEASED — not deleted — for the affected equipment so the
+ /// reserved external IDs free up while the audit row survives. Both run before the entity
+ /// rows they reference are removed so the id subqueries still resolve.
+ ///
///
///
public partial class CleanupSystemPlatformNamespaces : Migration
@@ -44,6 +53,58 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
-- stray equipment-scoped tag / Equipment could be bound to a SystemPlatform-namespace driver. Clean
-- both, plus anything that FKs into the affected Equipment (VirtualTag / ScriptedAlarm / state).
+-- Logical-reference cleanup that must run FIRST -------------------------------------------
+-- NodeAcl rows carry purely LOGICAL scope refs (ScopeKind + ScopeId, no physical FK). When the
+-- scoped entity is deleted below the ScopeId dangles and the ACL evaluator would still match it,
+-- silently granting/denying on a node that no longer exists. ScopeKind persists as a string via
+-- HasConversion, so the values below ('Namespace'/'Equipment'/'FolderSegment'/'Tag') are
+-- the literal enum member names. These deletes must read the entity-id subqueries while those rows
+-- still exist, hence they run before the entity DELETEs further down.
+
+-- Namespace-scoped grants pointing at the SystemPlatform namespaces themselves.
+DELETE FROM [NodeAcl]
+WHERE [ScopeKind] = 'Namespace'
+ AND [ScopeId] IN (
+ SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform');
+
+-- Equipment- and FolderSegment-scoped grants pointing at Equipment that hangs off the affected
+-- DriverInstances (both share Equipment.EquipmentId as the ScopeId).
+DELETE FROM [NodeAcl]
+WHERE [ScopeKind] IN ('Equipment', 'FolderSegment')
+ AND [ScopeId] IN (
+ SELECT [EquipmentId] FROM [Equipment]
+ WHERE [DriverInstanceId] IN (
+ SELECT [DriverInstanceId] FROM [DriverInstance]
+ WHERE [NamespaceId] IN (
+ SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
+
+-- Tag-scoped grants pointing at Tags bound to the affected DriverInstances (ScopeId = Tag.TagId).
+DELETE FROM [NodeAcl]
+WHERE [ScopeKind] = 'Tag'
+ AND [ScopeId] IN (
+ SELECT [TagId] FROM [Tag]
+ WHERE [DriverInstanceId] IN (
+ SELECT [DriverInstanceId] FROM [DriverInstance]
+ WHERE [NamespaceId] IN (
+ SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
+
+-- ExternalIdReservation is NOT generation-versioned and must outlive equipment deletion as a
+-- RELEASE, not a delete: active rows (ReleasedAt IS NULL) hold a fleet-wide unique reservation of
+-- the ZTag/SAPID under a filtered unique index. If we left them active after dropping the
+-- Equipment, the same external IDs could never be reused. Release them so the IDs free up while the
+-- audit row survives. Run BEFORE the Equipment DELETE so the EquipmentUuid subquery still resolves.
+UPDATE [ExternalIdReservation]
+SET [ReleasedAt] = SYSUTCDATETIME(),
+ [ReleasedBy] = 'CleanupSystemPlatformNamespaces',
+ [ReleaseReason] = 'Equipment removed by SystemPlatform-namespace cleanup migration (retired NamespaceKind).'
+WHERE [ReleasedAt] IS NULL
+ AND [EquipmentUuid] IN (
+ SELECT [EquipmentUuid] FROM [Equipment]
+ WHERE [DriverInstanceId] IN (
+ SELECT [DriverInstanceId] FROM [DriverInstance]
+ WHERE [NamespaceId] IN (
+ SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
+
-- Grandchildren of the affected Equipment ------------------------------------------------
-- ScriptedAlarmState is keyed by ScriptedAlarm.ScriptedAlarmId; remove before its ScriptedAlarm.
DELETE FROM [ScriptedAlarmState]