Skip to content

feat(organizations): sweep a joiner's owned workspaces into the org on join, disclose it at accept, and add external workspace invites - #5918

Merged
icecrasher321 merged 29 commits into
stagingfrom
staging-v8
Jul 30, 2026
Merged

feat(organizations): sweep a joiner's owned workspaces into the org on join, disclose it at accept, and add external workspace invites#5918
icecrasher321 merged 29 commits into
stagingfrom
staging-v8

Conversation

@icecrasher321

@icecrasher321icecrasher321 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Accepting an org invite now moves every workspace the invitee owns (archived included) into the organization — disclosed on the accept screen — with collaborators kept as seat-free external members and stale pre-move invites downgraded instead of escalating.

  • Invites land directly in the granted workspace, member-role org invites must carry workspace access (enforced route + domain layer), and the invite modal gains an explicit Member vs External choice.

  • The remove-member dialog now lists which owner-bound credentials will stop working, workspace-less members get a proper in-product empty state, and the membership docs reflect the new model.

Type of Change

  • New feature

Testing

Manually

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)

@vercel

vercelBot commented Jul 24, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedJul 30, 2026 2:17am

Request Review

@cursor

cursorBot commented Jul 24, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes org membership, invitation acceptance, workspace ownership transfer, seat/external billing rules, and removes legacy org invite APIs—high blast radius for access control and billing.

Overview
Organization membership and invitations are reworked so joining as an internal member moves the invitee’s owned workspaces (including archived) into the organization, with collaborators kept as external members. Accept flows (/invite, workspace switcher) show join previews (membership, seat use, workspaces that will move) and send disclosure tokens on accept; the server returns disclosure-outdated (409) if the sweep set changed since load.

Inviting consolidates on a shared InviteModal: multi-workspace grants, workspace access level, and Member / Admin / External membership. Organization-only POST invite routes are removed in favor of the workspace batch path. External invites require the invitee to already be on a paid plan (when billing is on); users in another org are forced external. Scoped revocation (DELETE with workspaceId) drops one grant from a multi-workspace pending invite; full cancel needs org admin or admin on every granted workspace. Member-role org invites must include at least one workspace.

Safety and parity: personal workspace creation re-checks org membership under a billing lock to avoid racing a join; admin member add mirrors invitation acceptance (transactional attach + Enterprise seat cap only). New GET …/removal-impact and UI warn which identity-bound credentials break on removal. Docs (permissions, workspaces, costs) describe the new model.

Minor: docs chat tool uses jsonSchema instead of Zod; breadcrumb typings in docs.

Reviewed by Cursor Bugbot for commit ea71c3c. Configure here.

@greptile-apps

greptile-appsBot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR expands organization membership and invitation behavior.

  • Moves a joining member’s owned workspaces into the organization while preserving collaborators as external members.
  • Adds acceptance-time migration disclosures and explicit member-versus-external workspace invitations.
  • Adds scoped invitation revocation, workspace landing behavior, and removal-impact disclosure.
  • Updates API contracts, billing and workspace policies, UI flows, tests, and membership documentation.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failures remain within the scope of the previous review threads.

No blocking failure remains.

Important Files Changed

FilenameOverview
apps/sim/lib/invitations/core.tsImplements invitation previews, disclosure validation, acceptance outcomes, and organization-join workspace migration orchestration.
apps/sim/lib/workspaces/organization-workspaces.tsAdds transactional helpers for attaching a joining user’s owned workspaces to an organization.
apps/sim/lib/billing/organizations/membership.tsExtends organization membership lifecycle and locking behavior used by join and removal flows.
apps/sim/app/api/v1/admin/organizations/[id]/members/route.tsAdds an atomic organization-membership and owned-workspace sweep to the administrative member-add path.
apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsxAdds explicit internal-member versus external-collaborator invitation controls.
apps/sim/app/invite/[id]/invite.tsxDisplays acceptance consequences and submits the disclosed workspace and membership outcome.
apps/sim/app/api/invitations/[id]/route.tsAdds invitation join previews and workspace-scoped revocation with corresponding authorization checks.
apps/sim/app/api/workspaces/route.tsCoordinates personal workspace creation with organization joins and handles default-creation races by refreshing workspace visibility.
apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsxAdds credential-impact loading and degraded-warning states to organization-member removal.
apps/sim/lib/api/contracts/invitations.tsExpands invitation request and response contracts for grants, membership outcomes, and acceptance disclosures.

Sequence Diagram

sequenceDiagram
participant Admin
participant Invitations
participant Invitee
participant Membership
participant Workspaces
Admin->>Invitations: Invite as Member/Admin or External
Invitations-->>Invitee: Show workspace grants and join disclosure
Invitee->>Invitations: Accept disclosed outcome
alt Internal organization member
Invitations->>Membership: Add organization membership
Membership->>Workspaces: Move owned personal workspaces into organization
Workspaces-->>Membership: Preserve collaborators as external members
else External collaborator
Invitations->>Workspaces: Grant selected workspace access only
end
Invitations-->>Invitee: Open granted workspace
Loading

Reviews (25): Last reviewed commit: "fix(invitations): restore the invite adm..." | Re-trigger Greptile

Comment threadapps/sim/app/api/invitations/[id]/route.ts
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 7509184. Configure here.

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/invitations/core.ts
Comment threadapps/sim/app/workspace/page.tsx Outdated
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/invitations/core.ts
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/app/api/workspaces/route.ts

@cursorcursorBot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5010800. Configure here.

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/app/api/invitations/[id]/accept/route.ts
Comment threadapps/sim/app/invite/[id]/invite.tsx
The preview returned one no-join shape for five different results — external
intent, already a member, a membership in another organization, a dead-grant
rejection, and a billing rejection. Two accumulating booleans could not separate
them, and the accept screen rendered the external copy for all of them: it told
people whose acceptance would fail with `upgrade-required` or
`workspace-not-found` that they were getting workspace access without a seat.
It now reports one `outcome`: `will-join` (a seat is taken), `already-member`
(only workspace access changes), `external` (never a seat), or `blocked`
(acceptance fails, so nothing is promised). `blocked` renders no membership
notice — silence is accurate where the external claim was false. The accept
button is deliberately left enabled: those cases already fail closed with the
correct error, and choosing what to actively tell someone whose organization's
payment lapsed is a product decision, not a review fix.
This also fixes a live mis-attribution the previous commit's guard introduced.
The consent check ran before the gates that produce the real cause, so a blocked
invitation returned `disclosure-outdated` — and the retry re-rendered the same
preview, leaving the invitee looping with no explanation. The guard now sits
after the dead-grant gate, and a disclosed `blocked` skips the comparison so the
billing gate below can surface `upgrade-required` instead.
The accept body carries `disclosedOutcome` in place of the boolean; the
membership comparison is unchanged (`will-join` versus a new membership being
created), so no acceptance that previously succeeded now fails.
Co-Authored-By: Claude <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/invitations/core.ts
The consent guard derived "a membership will be created" from
`shouldJoinOrganization`, which is still true at that point — it is only cleared
much later, after provisioning fails to yield a target organization. With billing
disabled and no organization on the workspace there is nothing to provision and
nothing to join, so the preview correctly reports `external` while the guard
computed `will-join`, rejecting every personal and grandfathered workspace invite
as `disclosure-outdated`. The retry rendered the same preview, so those invites
could not be accepted at all on billing-disabled deployments.
The predicate now mirrors the preview's own condition, so the two cannot drift.
Regression test asserts acceptance succeeds with billing off; it fails with
`disclosure-outdated` against the previous predicate.
Co-Authored-By: Claude <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/invitations/workspace-invitations.ts
Comment threadapps/sim/lib/invitations/core.ts
… invites blocked
The External paid-plan requirement is seat economics — an external collaborator
takes no seat, so somebody else must be paying for them. With billing disabled
there are no seats and no subscription rows at all, so every account resolves as
`free` and choosing External failed at send time and would have failed at accept.
Member and Admin still worked, so a self-hosted deployment had no way to grant
workspace-only access without an organization join and a workspace sweep. Both the
invite-time and accept-time gates now short-circuit when billing is off.
A route test asserted that rejection without setting `isBillingEnabled`, which
the shared mock defaults to false — it passed only because the gate ignored the
flag. It now opts in explicitly, since the rule it covers is billing-only.
Separately, the preview reported `external` for any invitee already in a
different organization, but acceptance only downgrades a workspace-kind invite
with live grants; an organization-kind invite hard-fails with
`already-in-organization`. Those now report `blocked`, so the screen stops
promising external access that acceptance can never grant. Legacy
organization-kind rows still exist and coalescing preserves that kind, so this is
reachable.
Co-Authored-By: Claude <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/invitations/core.ts Outdated
Comment threadapps/sim/lib/invitations/workspace-invitations.ts
…osure gaps
Privilege escalation. `createWorkspaceInvitation` stamped organization role
`admin` whenever the caller passed `membership: 'admin'`, but authorization only
checked workspace admin access — and unifying the invite modals exposed the Admin
option to any workspace admin, where it had previously been reachable only from
organization settings. A workspace-scoped administrator could therefore invite
someone who joins as an organization Admin, gaining admin on every workspace the
organization owns plus member and billing management. The inviter must now already
hold organization owner/admin, checked server-side because the batch endpoint is
reachable without the modal, and the modal no longer offers Admin to anyone else.
The preview promised external access without mirroring acceptance's
`external-requires-paid-plan` gate, so a free invitee — one who cancelled Pro, or
left the organization that forced the external invite — was told they had
workspace access and then refused. It now mirrors that gate, including its
exemptions (billing on, organization-owned workspace, externality not imposed),
and reports `blocked`.
The modal's Enterprise seat check counted every non-External email as a seat. The
server does not: an existing organization member is granted access directly, and
an invitee already in another organization is forced external. The hard block
refused batches the API would have accepted, so it is advisory now — per-email
failures already come back with reasons.
Co-Authored-By: Claude <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/invitations/disclosure-copy.ts Outdated
…come
Consolidating the modals dropped a permission check. The old workspace-header
modal derived `canInviteMembers` from `userPermissions.canAdmin` internally and
disabled its field and button; the shared modal takes `canInvite` as a prop that
defaults to true, and no call site passed it. Non-admins therefore saw a fully
enabled invite form and only learned otherwise when the server refused the send.
All three entry points now supply it: workspace admin for the header, the
existing `canManage` for the workspace Teammates page, and organization
owner/admin for organization settings.
When the join preview cannot be computed the outcome is unknown, and the callers
send no disclosure token — so acceptance runs without the consent guards. The
membership notice nevertheless asserted a seat-taking join from the sent intent,
which acceptance may resolve to external, already-a-member, or a failure. It is
conditional now ("If you're added to X as a member, that uses one of their
seats"), so the consequence is still disclosed without being claimed as settled.
Co-Authored-By: Claude <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ea71c3c. Configure here.

Comment threadapps/sim/lib/invitations/core.ts
Comment threadapps/sim/lib/invitations/send.ts
Sign up for freeto 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

@icecrasher321