Skip to content

feat(admin): move a workspace between organizations - #7254

Open
mzxchandra wants to merge 1 commit into
stagingfrom
feat/admin-workspace-org-transfer
Open

feat(admin): move a workspace between organizations#7254
mzxchandra wants to merge 1 commit into
stagingfrom
feat/admin-workspace-org-transfer

Conversation

@mzxchandra

Copy link
Copy Markdown
Contributor

Summary

Lets the admin panel move a workspace from one organization to another. The move was restricted to personal/grandfathered sources — assertWorkspaceMovable threw "Inter-organization workspace transfers are not supported" — so support could only re-home a workspace with manual SQL.

Most of the machinery already existed: durable operation IDs, seat-capacity checks, and changeWorkspaceStoragePayerInTx, which already accepted an arbitrary source payer, so the storage-ledger rebalance between two orgs needed no change. The work is a guard relaxation plus handling what the source organization loses.

Two invariants this feature could break, now enforced here

Moving a workspace between organizations is the first operation in the product capable of separating an artifact from the org that owns it. Two invariants nothing has ever had to defend:

A custom block and its bound workflow always share an organization. publishCustomBlock refuses a workflow outside the target org, so the pair has always been co-located. getCustomBlockAuthority resolves by the consumer's org, and admitCustomBlockChildExecution deliberately skips its concurrency reservation because "the consumer and source workspaces are always in the same organization". A stranded row would run a foreign tenant's workflow under its owner's credentials, billed to the wrong payer. The move unpublishes those blocks through the product's own deleteCustomBlock, and records CUSTOM_BLOCK_DELETED in the source org's audit view.

A fork parent and child always share an organization. assertCanFork pins the child to the source's org and resolveForkEdge has no org check at all. The move refuses while a cross-org edge would result; the fork must be disconnected first.

Three blockers

Each is evaluated in preflight and re-checked inside the transaction, because a subscription can lapse, a fork can be created, and an invitation can arrive in between.

Blocker Code Why
Cross-org fork edge fork-lineage-conflict Unlinking destroys promote history irreversibly; that call belongs to the customer, not a support admin mid-dialog
Live pending invitation pending-invitations-present Re-stamping an org-scoped invitation converts a pending membership in the source org into one in the destination, consuming a seat for an invitation the destination never issued. Expired invitations do not block
Entitlement downgrade destination-entitlement-downgrade Silent capability loss with no cleanup available and no signal to affected users

Blocking on invitations deleted the most intricate part of the design — no cross-org invitation migration, no intent resolver, no sibling-invitation creation. invitation-migration-plan.ts is untouched and the shipped personal→org invitation path is not modified at all.

Concurrency

Both organizations are now locked, ascending by id, mirroring acquireOrganizationUserMutationLocks. The source id is read optimistically before the transaction and re-verified under the locks, retrying through the existing loop via SourceOrganizationChangedError. Lock order is unchanged: invitation/workspace advisory locks → org locks → workspace row.

Audit

The workspace-scoped move entry resolves to the destination after the move, because buildOrgScopeCondition scopes org audit reads by the org's current workspaces. Without more, the org that lost the workspace would have no record. Adds an org-level entry (workspaceId: null + metadata.organizationId, that condition's other branch) so the loss is diagnosable from the side that incurred it.

Type of Change

  • New feature

Testing

Automated: full apps/sim suite — 37,306 tests across 2,669 files, zero failures. turbo run type-check clean across all 26 workspaces. check:api-validation:strict, check:boundaries, check:client-boundary, check:route-verbs, check:api-contract-routes, check:utils, check:import-specifiers, and the 39-audit check:audits bundle all pass.

11 new tests cover the org→org path: both org locks acquired in sorted order after the invitation locks and before the row lock; the payer transfer fenced on the source org read under those locks (the assertion that makes the whole move safe); each blocker refusing without mutating anything; custom blocks unpublished; the source-org audit entry landing in the source and not the destination; and the personal source path unchanged, as a regression fence.

End to end against a real database, two orgs and a workspace with content on both sides. Verified: org-owned workspaces appear as candidates with their source org named; every blocker refuses and mutates nothing; the happy path re-homes org, payer, and organizationAssignedAt; the move is reversible; replaying an operationId is idempotent; and both invariant post-conditions hold (zero cross-org custom blocks, zero cross-org fork edges).

E2E surfaced two defects the unit tests missed, both now with regression tests: getMovedWorkspaceSummary took an appliedContext parameter no call site passed, so every applied move reported sourceOrganization: null; and the source-org audit entries were specified but never implemented.

Reviewers should focus on the lock ordering in moveWorkspaceToOrganization and the expectedCurrentPayer fence — that optimistic check is what makes the payer transfer safe under concurrency.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Notes for reviewers

Not verifiable on a local deployment. resolveOrganizationEnterprisePlan short-circuits on isAccessControlEnabled && !isHosted, so every org resolves as entitled locally and the downgrade blocker cannot fire. That is correct — the gates it protects also all-pass there, so nothing is lost — but it means the blocker needs a hosted deployment to exercise. Related: isOrganizationOnEnterprisePlan is really "is on a paid org plan" (isOrgPlan = isTeam || isEnterprise), so Team is not a downgrade from Enterprise for these capabilities. The blocker reuses that same predicate deliberately, so it can never disagree with the gates it protects.

Companion PR in simstudioai/admin adds the review modal that presents all of this before the admin confirms.


Review history

Supersedes #7243, which is closed. That PR carried the same file state but had accumulated ~45 review threads across 10 rounds, and the bots had started re-reviewing the thread history rather than the diff — re-raising findings already fixed and regressing the score on unchanged code. This is a clean slate on identical code.

23 findings were fixed there before this PR was opened, every one a real bug except a single false positive that was pushed back on with the type definitions. The substantive ones:

  • Writer races. Enforcing the invariants at move time was never sufficient — publishCustomBlock and fork creation wrote without the lock the move holds, so either could commit after its scans. Both now serialize with it. The first fix for the fork was itself racy (the policy organization is captured before the transaction, and the lock is skipped entirely when it is null), so the parent is now row-locked instead, which also removed a lock-order inversion against invitation acceptance, a 5s timeout overwriting the fork's 10s, and an organization-wide lock held across the whole content copy.
  • Fork blocker gated on an organization source, so a personal workspace whose parent had since moved into one was blocked by preflight but not by the transaction.
  • Durable payload parser validated sourceOrganizationId then dropped it, so every reload would have claimed the source was not persisted.
  • usageCount is a union of live and deployed placements, so subtracting a live-only count misattributed blocks; both figures are now measured with identical predicates.
  • Entitlement fence, in four passes. Absence of a subscription row read as a pass; the plan was never checked; the fence rejected every move in the two deployment modes where entitlement is not subscription-backed; and billing-blocked organizations read as entitled. It now evaluates both organizations under the locks through a shared isSubscriptionBackedEntitlement predicate exported alongside the short-circuits it mirrors.
  • EMPTY_CREDENTIAL_SUMMARY omitted two newly required contract fields, which would have failed validation on every successful move.
  • Truncation was silent, then inconsistent: lists are now bounded with disclosed counts and a notice that opens "This review is incomplete".

@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 29, 2026 6:56am

Request Review

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

@greptile-apps

greptile-apps Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR enables administrators to move workspaces between organizations while preserving tenant, lineage, billing, entitlement, and audit invariants.

  • Serializes workspace moves with fork creation and custom-block publication.
  • Blocks moves that would create cross-organization fork edges, migrate pending invitation intent, or downgrade capabilities.
  • Cleans up source-organization artifacts and reports transfer impact through the admin API contract.
  • Adds regression coverage for lock ordering, payer fencing, blockers, cleanup, auditing, and personal-workspace behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/workspaces/admin-move.ts Orchestrates the cross-organization transfer with ordered locks, transactional blocker checks, payer fencing, cleanup, and audit recording.
apps/sim/lib/workspaces/admin-move-source-impact.ts Computes source-organization impact and performs transaction-aware cleanup of organization-scoped artifacts.
apps/sim/ee/workspace-forking/lib/create-fork.ts Re-reads the parent workspace under a conflicting row lock before inserting a fork, preventing stale organization policy from creating cross-organization lineage.
apps/sim/lib/workflows/custom-blocks/operations.ts Serializes custom-block publication with organization mutation and supports transactional deletion during workspace moves.
apps/sim/lib/api/contracts/v1/admin/dashboard-workspaces.ts Expands the admin transfer contract with source impact, credential, entitlement, blocker, notice, and truncation metadata.
apps/sim/lib/billing/core/subscription.ts Exposes whether organization entitlement is subscription-backed so transfer checks respect deployment-configured entitlement modes.

Sequence Diagram

sequenceDiagram
    participant Admin
    participant Move as Workspace move
    participant Source as Source organization
    participant Destination as Destination organization
    participant Workspace as Workspace row

    Admin->>Move: Request transfer
    Move->>Move: Acquire invitation locks
    Move->>Source: Lock source organization
    Move->>Destination: Lock destination organization
    Move->>Workspace: Lock and re-read current organization
    Move->>Move: Re-check forks, invitations, and entitlements
    alt Blocker exists
        Move-->>Admin: Refuse without mutation
    else Transfer allowed
        Move->>Source: Remove source-scoped artifacts
        Move->>Workspace: Change organization and storage payer
        Move->>Source: Record source loss audit entry
        Move->>Destination: Record destination move entry
        Move-->>Admin: Return applied move summary
    end
Loading

Reviews (5): Last reviewed commit: "feat(admin): move a workspace between or..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 8 files

Confidence score: 2/5

  • apps/sim/ee/workspace-forking/lib/create-fork.ts only makes concurrent organization attachment wait, so after the fork commits attachOwnedWorkspacesToOrganizationTx can attach the parent alone during a personal-parent fork. Coordinate the lock and post-fork attachment flow to preserve the intended workspace set.
  • apps/sim/lib/workspaces/admin-move.ts treats Team as entitled via isOrgPlan, allowing Enterprise-to-Team moves that resolveMoveEntitlements rejects. Align the transaction fence with the entitlement resolution logic.
  • apps/sim/lib/workspaces/admin-move-source-impact.ts reports a branding change when one organization has missing whitelabel settings and the other has {} despite equivalent default branding. Normalize missing and empty settings before comparison.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/workspaces/admin-move-source-impact.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move-source-impact.ts:535">
P3: When one organization has no whitelabel settings and the other stores an empty settings object, this reports a branding change even though both use the default branding. Normalize missing and empty settings before comparing them.</violation>
</file>

<file name="apps/sim/lib/workspaces/admin-move.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move.ts:1188">
P1: When the source is Enterprise and the destination is Team, this transaction fence treats both organizations as entitled because `isOrgPlan` includes Team, so it allows a move that `resolveMoveEntitlements` reports would lose Enterprise capabilities. It also incorrectly blocks Team→Free moves; use the same Enterprise-specific predicate as `resolveMoveEntitlements` for this re-check.</violation>
</file>

<file name="apps/sim/ee/workspace-forking/lib/create-fork.ts">

<violation number="1" location="apps/sim/ee/workspace-forking/lib/create-fork.ts:194">
P1: When a personal parent is forked by another admin, this lock only makes a concurrent organization attachment wait. After the fork commits, `attachOwnedWorkspacesToOrganizationTx` can attach the parent alone, leaving its child personal and violating the fork invariant; check the edge before attachment or move both atomically.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/ee/workspace-forking/lib/create-fork.ts
Comment thread apps/sim/lib/workspaces/admin-move.ts Outdated
Comment thread apps/sim/lib/workspaces/admin-move-source-impact.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 8 files

Confidence score: 2/5

  • apps/sim/ee/workspace-forking/lib/create-fork.ts can commit a child fork while an organization attach/detach batch is waiting on the row lock, after which the batch updates only its stale parent ID. Preserve the lock/transaction ordering so the batch cannot resume against stale workspace state.
  • apps/sim/lib/workspaces/admin-move.ts misclassifies Team and Enterprise plans for subscription-backed billing, allowing Enterprise→Team moves past the downgrade fence while blocking Team→free moves. Reuse the Enterprise-specific classification for the downgrade check.
  • The move preflight contract is inconsistent across apps/sim/lib/api/contracts/v1/admin/dashboard-workspaces.ts and apps/sim/lib/workspaces/admin-move.ts: seat-capacity failures are exposed as warnings despite transaction rejection, and fork/credential truncation can omit the incomplete-review notice. Align blockers and notices with the conditions that can prevent or limit a move.
  • apps/sim/lib/workspaces/admin-move-source-impact.ts can omit a source-organization cap when collaborator joins exceed 1,000 and can issue up to 2,000 enrichment queries concurrently for 500 custom blocks. Apply the usage-limit inner join and bounded-concurrency enrichment before relying on these preflight results.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/ee/workspace-forking/lib/create-fork.ts">

<violation number="1" location="apps/sim/ee/workspace-forking/lib/create-fork.ts:194">
P1: When organization attach or detach has already snapshotted workspace IDs and is waiting on this row lock, the fork can commit a child before that batch resumes. The batch then updates only its stale parent ID, leaving the parent and child in different organizations; synchronize these operations with fork creation or re-enumerate children after acquiring the locks.</violation>
</file>

<file name="apps/sim/lib/api/contracts/v1/admin/dashboard-workspaces.ts">

<violation number="1" location="apps/sim/lib/api/contracts/v1/admin/dashboard-workspaces.ts:205">
P2: When destination seat capacity is exceeded, preflight returns a non-null `warning` but leaves `blockers` empty. The move transaction then throws `seat-capacity-exceeded`, so a UI following this new field can offer a confirmation guaranteed to fail. Add that capacity message to `blockers`, or explicitly make the UI honor `warning`.</violation>
</file>

<file name="apps/sim/lib/workspaces/admin-move.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move.ts:675">
P2: When only fork edges or credential/environment-variable keys are truncated, preflight returns nonzero truncation counts without the required incomplete-review notice. Pass the merged truncation state into notice generation and check credential truncation even when no source organization exists.</violation>

<violation number="2" location="apps/sim/lib/workspaces/admin-move.ts:1188">
P1: When billing is subscription-backed, this recheck treats Team plans as equivalent to Enterprise. An Enterprise→Team move bypasses the downgrade fence, while a Team→free move is incorrectly blocked; reuse the Enterprise-only predicate here.</violation>
</file>

<file name="apps/sim/lib/workspaces/admin-move-source-impact.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move-source-impact.ts:231">
P2: For 500 source custom blocks, this starts up to 2,000 database queries concurrently and can saturate the pool or time out the admin preflight. Enrich blocks with bounded concurrency instead of one unbounded `Promise.all`.</violation>

<violation number="2" location="apps/sim/lib/workspaces/admin-move-source-impact.ts:410">
P2: When a workspace has more than 1,000 explicit collaborators, this join returns uncapped users too, so the bounded preflight list can omit a real source-org cap. Join the usage-limit table as an inner join before applying the limit.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread apps/sim/ee/workspace-forking/lib/create-fork.ts
Comment thread apps/sim/lib/workspaces/admin-move.ts Outdated
Comment thread apps/sim/lib/api/contracts/v1/admin/dashboard-workspaces.ts
Comment thread apps/sim/lib/workspaces/admin-move.ts Outdated
Comment thread apps/sim/lib/workspaces/admin-move-source-impact.ts Outdated
Comment thread apps/sim/lib/workspaces/admin-move-source-impact.ts
@mzxchandra
mzxchandra force-pushed the feat/admin-workspace-org-transfer branch from 8cf3b11 to a5cead5 Compare August 29, 2026 03:16
@mzxchandra
mzxchandra force-pushed the feat/admin-workspace-org-transfer branch from a5cead5 to 361d1b1 Compare August 29, 2026 05:32
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/ee/workspace-forking/lib/create-fork.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 existing issues remain and 2 new issues found across 9 files

Confidence score: 2/5

  • apps/sim/lib/workspaces/admin-move-source-impact.ts can allow moves into a past_due Enterprise subscription even though isOrganizationOnEnterprisePlan considers it unusable, creating an entitlement mismatch and potentially permitting an invalid move — align the query with the entitlement check.
  • apps/sim/lib/workspaces/organization-workspaces.ts captures workspaceIds before recursively created fork descendants are appended, so grandchildren can be omitted from the move and left behind — walk the complete descendant closure under locks or revalidate before applying changes.
  • apps/sim/lib/workspaces/organization-workspaces.ts excludes archived or admin-forked children while still moving their attached parent, which can split a fork hierarchy across organizations — include every fork descendant in the move validation.
  • apps/sim/lib/workspaces/admin-move.ts may report a stale downgrade in the successful move summary when Enterprise entitlement changes after the optimistic read, despite the fenced check allowing the move — derive the summary from the fenced entitlement result.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/workspaces/admin-move-source-impact.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move-source-impact.ts:402">
P1: When the destination Enterprise subscription is `past_due`, this query marks it as Enterprise and lets the move proceed, but `isOrganizationOnEnterprisePlan` treats that subscription as unusable and Enterprise-gated features become unavailable after the move. Use the same usable-status predicate as the feature gates when resolving destination entitlements.</violation>
</file>

<file name="apps/sim/lib/workspaces/admin-move.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move.ts:1170">
P2: When the destination gains Enterprise entitlement after the optimistic read, the fenced check allows the move but the applied summary still reports the stale downgrade. Use the fenced entitlement result for the successful summary so the response matches the decision that allowed the move.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/workspaces/admin-move-source-impact.ts Outdated
Comment thread apps/sim/lib/workspaces/admin-move.ts Outdated
@mzxchandra
mzxchandra force-pushed the feat/admin-workspace-org-transfer branch from 361d1b1 to a47703a Compare August 29, 2026 06:11
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 3 new issues found across 9 files

Confidence score: 2/5

  • apps/sim/lib/workspaces/organization-workspaces.ts closes only one fork level, so a grandchild created after the snapshot can remain in the old organization; traverse and lock the full descendant closure before attaching the parent.
  • apps/sim/lib/workspaces/organization-workspaces.ts row-locks swept children that were absent from the initial workspace list without taking their workspace advisory locks, leaving a concurrency gap; include all swept children in the advisory-lock plan.
  • apps/sim/lib/workspaces/organization-workspaces.ts can attach a fork created after disclosure validation without rechecking disclosedWorkspaceIds, potentially bypassing invitation disclosure requirements; revalidate the final swept set before commit.
  • apps/sim/lib/workspaces/organization-workspaces.ts excludes fork children owned by another workspace admin from the attachability check, allowing a cross-organization fork edge after the parent moves; validate every fork child and reject or explicitly handle violations.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/workspaces/organization-workspaces.ts">

<violation number="1" location="apps/sim/lib/workspaces/organization-workspaces.ts:280">
P1: The sweep only closes one fork level, so a child created after the snapshot can have a grandchild that remains in the old organization. Traverse and lock the full descendant closure before attaching the parent.</violation>

<violation number="2" location="apps/sim/lib/workspaces/organization-workspaces.ts:288">
P1: When a fork child is absent from the caller’s initial workspace list, this query row-locks it after the organization lock without acquiring its workspace advisory lock. Include swept children in the advisory-lock plan before taking organization locks, or restart when the lock set expands.</violation>

<violation number="3" location="apps/sim/lib/workspaces/organization-workspaces.ts:292">
P1: During invitation acceptance, a fork created after disclosure validation can be swept here and attached without updating or rechecking `disclosedWorkspaceIds`. Revalidate the final swept set before committing, or abort when the sweep expands the consented set.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread apps/sim/lib/workspaces/organization-workspaces.ts Outdated
Comment thread apps/sim/lib/workspaces/organization-workspaces.ts Outdated
Comment thread apps/sim/lib/workspaces/organization-workspaces.ts Outdated
Comment thread apps/sim/lib/workspaces/admin-move-source-impact.ts Outdated
Comment thread apps/sim/lib/workspaces/admin-move.ts
Comment thread apps/sim/lib/workspaces/admin-move.test.ts Outdated
@mzxchandra
mzxchandra force-pushed the feat/admin-workspace-org-transfer branch from a47703a to 654030f Compare August 29, 2026 06:40
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/workspaces/admin-move-source-impact.test.ts Outdated
The admin workspace move was restricted to personal/grandfathered sources;
`assertWorkspaceMovable` refused anything already owned by an organization, so
support could only re-home a workspace with manual SQL.

Relax that guard to a drift-only check and handle the source organization.
`changeWorkspaceStoragePayerInTx` already accepted an arbitrary source payer,
so the storage-ledger rebalance needed no change.

Moving a workspace between organizations is the first operation capable of
separating an artifact from the organization that owns it, so two invariants
nothing has ever had to defend are enforced here:

- A custom block and its bound workflow always share an organization.
  `getCustomBlockAuthority` resolves by the consumer's org and
  `admitCustomBlockChildExecution` skips its concurrency reservation on the
  strength of that, so a stranded row would run a foreign tenant's workflow
  under its owner's credentials, billed to the wrong payer. The move
  unpublishes those blocks through the product's own `deleteCustomBlock` and
  records the loss in the source organization's audit view.
- A fork parent and child always share an organization. `resolveForkEdge` has
  no org check at all, so the move refuses while a cross-org edge would result.

Move-time checks alone cannot hold either invariant, because the writers can
commit after them. `publishCustomBlock` now validates and inserts under the
organization mutation lock the move holds, and `createFork` row-locks its
parent so the move and the organization-attach path — both of which take
`FOR NO KEY UPDATE` on that row — serialize against it.

Pending invitations block: re-stamping an org-scoped invitation would convert
a pending membership in the source org into one in the destination, consuming
a seat for an invitation the destination never issued.

An entitlement downgrade blocks. When entitlement is subscription-backed, both
organizations are re-evaluated under the locks rather than trusting a
pre-transaction read that is stale in both directions. The two modes where
`resolveOrganizationEnterprisePlan` grants entitlement by deployment
configuration are excluded through a shared `isSubscriptionBackedEntitlement`
predicate, so a missing subscription row is never misread as a lapse.

Both organizations are locked, ascending by id, mirroring
`acquireOrganizationUserMutationLocks`. The source id is read optimistically
before the transaction and re-verified under the locks, retrying through the
existing loop when it moved.
@mzxchandra
mzxchandra force-pushed the feat/admin-workspace-org-transfer branch from 654030f to 7809360 Compare August 29, 2026 06:56
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 9 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant