Skip to content

feat(governance): external workspace users from outside org#4313

Merged
icecrasher321 merged 10 commits intostagingfrom
feat/external-org-ws
Apr 28, 2026
Merged

feat(governance): external workspace users from outside org#4313
icecrasher321 merged 10 commits intostagingfrom
feat/external-org-ws

Conversation

@icecrasher321
Copy link
Copy Markdown
Collaborator

@icecrasher321 icecrasher321 commented Apr 27, 2026

Summary

External Workspace Users allowed if they already belong to another org. Do not count against your own org seat count. Clearly tagged as external.

Type of Change

  • New feature

Testing

Tested 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
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 28, 2026 4:57am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 27, 2026

PR Summary

High Risk
Touches membership, invitations, seat counting, and Stripe outbox sync logic; mistakes could mis-bill seats or improperly grant/revoke workspace access. Also adds new transactional revocation/ownership-transfer paths that can affect permissions and credentials across multiple workspaces.

Overview
Enables inviting external workspace members (users who already belong to another org) to organization-owned workspaces without adding them to the org roster as seat-consuming members, and propagates membershipIntent through invitation APIs, audit metadata, telemetry, and UI badges.

Reworks workspace invitations to a server-side POST /api/workspaces/invitations/batch flow (replacing the prior per-email POST), returning per-invite success/failure and allowing “internal” invites to fall back to external access on org-membership conflicts; invitation updates now block role changes for external invites.

Updates org roster and seat/billing calculations to exclude external invites from seat occupancy while still listing external workspace-access holders, adds removal support for external members (revoking workspace permissions, permission-group membership, and credential memberships), and introduces optional post-removal seat reduction with a new Stripe outbox handler to sync subscription seat quantities.

Reviewed by Cursor Bugbot for commit 774caa1. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR introduces external workspace members: existing Sim users who already belong to another organisation can be invited to a workspace without joining the host org, consuming a seat, or appearing in the org roster. The core changes are a new membership_intent column on the invitation table ('internal' | 'external'), updated invite creation logic that auto-classifies cross-org invitees as external, updated acceptInvitation that skips org-join steps for external intents, an extended org roster endpoint that surfaces external members, and a new removeExternalUserFromOrganizationWorkspaces helper for cleanup.

Confidence Score: 5/5

Safe to merge; all P2 findings are low-probability edge cases or clarity improvements with no data-integrity risk.

All findings are P2. The seat-exclusion logic is correctly guarded by the NOT NULL migration default; transfer-ownership correctly filters externals; the invite/accept/remove paths are logically sound. The one status-code mismatch (500 vs 409 on a race-condition guard) and the archived-workspace inconsistency are minor operational concerns that do not affect correctness under normal usage.

apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts — status-code mapping for the external-removal guard; apps/sim/lib/billing/organizations/membership.ts — archived workspace filter inconsistency.

Important Files Changed

Filename Overview
apps/sim/app/api/workspaces/invitations/route.ts Adds membershipIntent determination at invite-creation time: existing Sim users who already belong to a different org get 'external' intent (no seat consumed); new users and org-less users go through seat validation as before.
apps/sim/lib/invitations/core.ts Updated acceptInvitation to skip ensureUserInOrganization and setActiveOrganizationForCurrentSession when membershipIntent === 'external'; workspace permissions are still granted via grants.
apps/sim/lib/billing/organizations/membership.ts Adds removeExternalUserFromOrganizationWorkspaces for revoking all workspace-level access for non-org-member users; includes archived workspaces in workspace set (minor inconsistency with roster).
apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts DELETE handler extended to fall through to external-member removal when target user has no org-member row; status-code mapping for the internal safety guard returns 500 instead of 409 for the race-condition case.
apps/sim/app/api/organizations/[id]/roster/route.ts Roster now queries workspace-permissions for users without an org-member row; external members assembled with synthetic memberId: 'external-{userId}' and role: 'external'; pending external invitations included with correct role label.
apps/sim/lib/billing/validation/seat-management.ts All pending-invitation seat counts now exclude rows where membershipIntent = 'external' via ne(invitation.membershipIntent, 'external'), safely covered by the NOT NULL DEFAULT 'internal' migration.
packages/db/migrations/0199_invitation_membership_intent.sql Adds invitation_membership_intent enum (`internal
apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/transfer-ownership-dialog/transfer-ownership-dialog.tsx Correctly filters out external members (m.role !== 'external') from ownership-transfer candidates.
apps/sim/lib/workspaces/organization/utils.ts calculateSeatUsage updated to filter out external pending invitations from the seat count; relies on invitations having membershipIntent present (intentionally optional per developer note).

Sequence Diagram

sequenceDiagram
    participant Admin
    participant InviteAPI as POST /workspaces/invitations
    participant DB
    participant Email
    participant Invitee

    Admin->>InviteAPI: invite email, workspaceId, permission
    InviteAPI->>DB: lookup existingUser by email
    alt User exists & in a different org
        InviteAPI->>DB: getUserOrganization(existingUser.id)
        DB-->>InviteAPI: existingOrgId != invitePolicy.organizationId
        Note over InviteAPI: membershipIntent = 'external'<br/>skip seat validation
    else User exists, no org
        InviteAPI->>DB: validateSeatAvailability
        Note over InviteAPI: membershipIntent = 'internal'
    else User does not exist
        InviteAPI->>DB: validateSeatAvailability
        Note over InviteAPI: membershipIntent = 'internal'
    end
    InviteAPI->>DB: createPendingInvitation(membershipIntent)
    InviteAPI->>Email: sendInvitationEmail
    Email-->>Invitee: invitation link

    Invitee->>DB: acceptInvitation(token)
    alt membershipIntent = 'external'
        Note over DB: skip ensureUserInOrganization<br/>skip setActiveOrg<br/>grant workspace permissions only
    else membershipIntent = 'internal'
        DB->>DB: ensureUserInOrganization (seat consumed)
        DB->>DB: grant workspace permissions
        DB->>DB: setActiveOrganization
    end
    DB-->>Invitee: redirect to workspace

    Admin->>DB: DELETE /organizations/:id/members/:userId
    DB->>DB: check targetMember in org
    alt is org member
        DB->>DB: removeUserFromOrganization (full billing logic)
    else is external (no org-member row)
        DB->>DB: removeExternalUserFromOrganizationWorkspaces
    end
Loading

Reviews (2): Last reviewed commit: "edge case improvements" | Re-trigger Greptile

Comment thread apps/sim/app/api/organizations/[id]/roster/route.ts Outdated
Comment thread apps/sim/lib/workspaces/organization/utils.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/app/api/workspaces/members/[id]/route.ts
Comment thread apps/sim/lib/billing/organizations/seats.ts Outdated
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/app/api/workspaces/invitations/route.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/app/api/workspaces/members/[id]/route.ts
Comment thread apps/sim/hooks/queries/invitations.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/lib/billing/validation/seat-management.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 4b0b362. Configure here.

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 and found 1 potential issue.

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 774caa1. Configure here.

Comment thread apps/sim/hooks/queries/workspace.ts
@icecrasher321 icecrasher321 merged commit 2e3de9a into staging Apr 28, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/external-org-ws branch April 28, 2026 06:54
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