Skip to content

feat(clerk-js,ui): Auto-activate exclusive organization in choose-organization task#8933

Merged
LauraBeatris merged 4 commits into
mainfrom
nicolas/orgs-1643-clerk-js-auto-activate-exclusive
Jun 19, 2026
Merged

feat(clerk-js,ui): Auto-activate exclusive organization in choose-organization task#8933
LauraBeatris merged 4 commits into
mainfrom
nicolas/orgs-1643-clerk-js-auto-activate-exclusive

Conversation

@NicolasLopes7

@NicolasLopes7 NicolasLopes7 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

When the choose-organization session task fires for a member of an exclusive-membership organization, clerk-js now skips the org picker entirely: it shows the loading spinner and auto-calls setActive on that org. The generic instance-wide force-choose-org flow is unchanged for everyone else.

Implements the clerk-js half of exclusive membership "after-auth".

Changes

  • Expose Organization.exclusiveMembership — parse the FAPI exclusive_membership field (@clerk/shared types + clerk-js Organization resource fromJSON / __internal_toSnapshot). Field is undefined for non-adopting instances.
  • TaskChooseOrganization — detect the membership whose organization.exclusiveMembership === true (an exclusive member belongs to exactly one org). If found: render only the spinner and auto-setActive once (useRef single-fire guard), reusing the existing navigateOnSetActive / redirectUrlComplete wiring. On failure, surface the error via card state and fall back to the normal flows so the user can recover. No exclusive membership → existing behavior, zero change.

Summary by CodeRabbit

Release Notes

New Features

  • Organizations can now enforce exclusive membership, restricting users to a single organization at a time
  • Users in exclusive-membership organizations are automatically activated during sign-in, eliminating the need to manually select an organization

…anization task

Parse the `exclusive_membership` field on the Organization resource and expose
it as `Organization.exclusiveMembership`.

When the `choose-organization` session task fires for a member of an
organization that enforces exclusive membership, skip the org picker and
auto-call setActive on that organization behind the existing loading spinner.
If activation fails, surface the error via card state and fall back to the
regular flows so the user can recover. The generic, instance-wide
force-choose-organization path is unchanged when the field is absent or false.
@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 232e4fe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
Name Type
@clerk/clerk-js Minor
@clerk/shared Minor
@clerk/ui Minor
@clerk/chrome-extension Patch
@clerk/expo Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/headless Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/vue Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 19, 2026 6:34pm
swingset Ready Ready Preview, Comment Jun 19, 2026 6:34pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds exclusiveMembership boolean to the OrganizationJSON DTO, OrganizationResource interface, and Organization class (with fromJSON/snapshot support). Updates TaskChooseOrganization to detect an exclusive-membership organization from the user's memberships and automatically call setActive instead of showing the org picker.

Changes

Exclusive Membership Auto-Activation

Layer / File(s) Summary
Organization type contract and resource implementation
packages/shared/src/types/json.ts, packages/shared/src/types/organization.ts, packages/clerk-js/src/core/resources/Organization.ts, .changeset/exclusive-membership-auto-activate.md
Adds exclusive_membership?: boolean to OrganizationJSON, adds documented exclusiveMembership: boolean to OrganizationResource, and populates the field in Organization class initialization, fromJSON, and __internal_toSnapshot. Changeset records the minor version bumps.
TaskChooseOrganization auto-activation logic
packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx
Derives exclusiveOrganization from user memberships, tracks activation state via hasAutoActivated/autoActivateFailed/shouldAutoActivate, and adds a useEffect that calls setActive and routes via navigateOnSetActive when eligible. Updates the disabled-screen guard and switches the loading spinner to also trigger on shouldAutoActivate. Introduces LoadingCardContent as a shared spinner layout.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A bunny with one warren, no more roaming free,
The exclusive org calls — "You belong here with me!"
A boolean field, a snapshot, a JSON key,
setActive fires and whisks the user away.
No picker to ponder, no screen to dismiss,
Just a spinner that spins into membership bliss! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature: auto-activation of exclusive organizations in the choose-organization task, which is the primary change across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8933

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8933

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8933

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8933

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8933

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8933

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8933

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8933

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8933

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8933

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8933

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8933

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8933

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8933

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8933

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8933

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8933

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8933

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8933

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8933

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8933

commit: 232e4fe

The optional `exclusiveMembership?: boolean` broke the `@clerk/shared`
typecheck: `PrimitiveKeys<OrganizationResource>` is a homomorphic mapped
type, so an optional property preserves its `?` modifier and injects
`undefined` into the indexed-access union, violating the
`LooseExtractedParams<T extends string>` constraint in clerk.ts.

Mirror `selfServeSSOEnabled` (also an instance-gated `*bool,omitempty`
on the backend): required `boolean`, defaulted to `false` when the FAPI
field is absent. Detection (`organization.exclusiveMembership === true`)
is unchanged.
Comment thread .changeset/exclusive-membership-auto-activate.md Outdated
@LauraBeatris LauraBeatris changed the title feat(clerk-js,ui): auto-activate exclusive organization in choose-organization task feat(clerk-js,ui): Auto-activate exclusive organization in choose-organization task Jun 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-19T18:35:51.086Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 2
🔴 Breaking changes 0
🟡 Non-breaking changes 1
🟢 Additions 2

🤖 This report was reviewed by claude-sonnet-4-6.


@clerk/nuxt

Current version: 2.6.5
Recommended bump: MINOR → 2.7.0

Subpath ./components

🟡 Non-breaking Changes (1)

Modified: CreateOrganization
// ... 1 unchanged line elided ...
    path: string | undefined;
    routing?: Extract<import("@clerk/shared/types").RoutingStrategy, "path">;
  } & {
-   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":pathRoot");
+   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":exclusiveMembership" | ":pathRoot");
    skipInvitationScreen?: boolean;
    appearance?: import("@clerk/shared/types").ClerkAppearanceTheme;
  }) | ({
    path?: never;
    routing?: Extract<import("@clerk/shared/types").RoutingStrategy, "hash">;
  } & {
-   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":pathRoot");
+   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":exclusiveMembership" | ":pathRoot");
    skipInvitationScreen?: boolean;
    appearance?: import("@clerk/shared/types").ClerkAppearanceTheme;
  }), {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<({
    path: string | undefined;
    routing?: Extract<import("@clerk/shared/types").RoutingStrategy, "path">;
  } & {
-   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":pathRoot");
+   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":exclusiveMembership" | ":pathRoot");
    skipInvitationScreen?: boolean;
    appearance?: import("@clerk/shared/types").ClerkAppearanceTheme;
  }) | ({
    path?: never;
    routing?: Extract<import("@clerk/shared/types").RoutingStrategy, "hash">;
  } & {
-   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":pathRoot");
+   afterCreateOrganizationUrl?: ((organization: import("@clerk/shared/types").OrganizationResource) => string) | ((string & Record<never, never>) | ":name" | ":id" | ":slug" | ":imageUrl" | ":hasImage" | ":membersCount" | ":pendingInvitationsCount" | ":adminDeleteEnabled" | ":maxAllowedMemberships" | ":selfServeSSOEnabled" | ":exclusiveMembership" | ":pathRoot");
    skipInvitationScreen?: boolean;
    appearance?: import("@clerk/shared/types").ClerkAppearanceTheme;
  })> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>

Static analyzer: Breaking change in variable CreateOrganization: Type changed: import("@vue/runtime-core").DefineComponent<({afterCreateOrganizationUrl?:(":adminDeleteEnabled"|":hasImage"|":id"|":im…import("@vue/runtime-core").DefineComponent<({afterCreateOrganizationUrl?:(":adminDeleteEnabled"|":exclusiveMembership"…

🤖 AI review (reclassified as non-breaking) (95%): The only change is adding a new string literal ":exclusiveMembership" to the union type of the optional afterCreateOrganizationUrl prop, which widens the accepted input type — existing consumers passing any previously valid value continue to compile correctly.


@clerk/shared

Current version: 4.19.1
Recommended bump: MINOR → 4.20.0

Subpath ./types

🟢 Additions (2)

Added: OrganizationJSON.exclusive_membership
+ exclusive_membership?: boolean;

Added property OrganizationJSON.exclusive_membership

Added: OrganizationResource.exclusiveMembership
+ exclusiveMembership: boolean;

Added property OrganizationResource.exclusiveMembership


Report generated by Break Check

Last ran on 232e4fe.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/shared/src/types/organization.ts (1)

100-103: Flag this public JSDoc change for Docs-team review.

This updates customer-facing JSDoc for OrganizationResource.exclusiveMembership; please confirm the generated reference docs output is reviewed for wording/behavior alignment.

As per coding guidelines, packages/** treats JSDoc on public/reference-facing APIs as customer-facing docs and asks for Docs-team review when those docs change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/organization.ts` around lines 100 - 103, The JSDoc
for the public API property exclusiveMembership in the OrganizationResource type
has been updated, which constitutes a customer-facing documentation change per
coding guidelines for packages/**. Flag this change for the Docs-team to review
and ensure they validate that the generated reference documentation accurately
reflects the updated JSDoc wording and correctly describes the behavior of the
exclusiveMembership flag regarding exclusive organizational membership
enforcement.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx`:
- Around line 77-80: In the catch block that handles the auto-activation error,
reorder the statements so that setAutoActivateFailed(true) is called before
handleError(err, [], card.setError). Since handleError rethrows unknown errors,
placing setAutoActivateFailed after it may prevent the fallback picker state
from being set. Move setAutoActivateFailed(true) to execute first in the catch
block to ensure the fallback behavior is triggered regardless of whether
handleError rethrows.

---

Nitpick comments:
In `@packages/shared/src/types/organization.ts`:
- Around line 100-103: The JSDoc for the public API property exclusiveMembership
in the OrganizationResource type has been updated, which constitutes a
customer-facing documentation change per coding guidelines for packages/**. Flag
this change for the Docs-team to review and ensure they validate that the
generated reference documentation accurately reflects the updated JSDoc wording
and correctly describes the behavior of the exclusiveMembership flag regarding
exclusive organizational membership enforcement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 1c6f9abf-7b63-4337-a547-6857f63da5f1

📥 Commits

Reviewing files that changed from the base of the PR and between d0ec3d0 and 232e4fe.

📒 Files selected for processing (5)
  • .changeset/exclusive-membership-auto-activate.md
  • packages/clerk-js/src/core/resources/Organization.ts
  • packages/shared/src/types/json.ts
  • packages/shared/src/types/organization.ts
  • packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx

Comment on lines +77 to +80
} catch (err: any) {
handleError(err, [], card.setError);
setAutoActivateFailed(true);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set fallback state before delegating to handleError in the auto-activation catch path.

handleError rethrows unknown errors, so in the current order setAutoActivateFailed(true) may never run. That breaks the intended “fallback to picker on activation failure” behavior.

💡 Proposed fix
-      } catch (err: any) {
-        handleError(err, [], card.setError);
-        setAutoActivateFailed(true);
+      } catch (err: unknown) {
+        setAutoActivateFailed(true);
+        try {
+          handleError(err, [], card.setError);
+        } catch {
+          card.setError('Something went wrong while activating your organization. Please choose one manually.');
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx`
around lines 77 - 80, In the catch block that handles the auto-activation error,
reorder the statements so that setAutoActivateFailed(true) is called before
handleError(err, [], card.setError). Since handleError rethrows unknown errors,
placing setAutoActivateFailed after it may prevent the fallback picker state
from being set. Move setAutoActivateFailed(true) to execute first in the catch
block to ensure the fallback behavior is triggered regardless of whether
handleError rethrows.

@LauraBeatris LauraBeatris enabled auto-merge (squash) June 19, 2026 18:39
@LauraBeatris LauraBeatris merged commit 01789b4 into main Jun 19, 2026
83 of 91 checks passed
@LauraBeatris LauraBeatris deleted the nicolas/orgs-1643-clerk-js-auto-activate-exclusive branch June 19, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants