feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware - #3977

Merged
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync
Oct 8, 2024
Merged

feat(nextjs): Handle URL <> Session Org Mismatch in Middleware#3977
izaaklauer merged 56 commits into
mainfrom
izaak/ORGS-132-middleware-orgsync

Conversation

@izaaklauer

@izaaklauerizaaklauer commented Aug 16, 2024

Copy link
Copy Markdown
Contributor

What problem is this solving?

Today, when developers use organization slugs in URLs, they need to go through contortions to be able to depend on the organization data in the clerk session, especially in server-rendered pages. In short, they need to detect the mismatch between the URL and the session on the server component, punt back to client-side javascript that can call setActive for the org as specified by the URL, and then re-render the server component. See some of that workflow here: https://clerk.com/docs/guides/force-organizations#set-an-active-organization-based-on-the-url

What changed?

This PR introduces a new NextJS middleware option that allows developers to specify their URL structure to the middleware - specifically which url patterns indicate a desire to activate the personal workspace or an organization, and if so which one (by slug or ID).

The middleware then checks the actual URL against that pattern, and if it detects a mismatch between the desired-active organization (from the url) and the actually-active organization (from the session), it performs a handshake with a new query param, which will activate the new organization.

What does the new API look like?

Imagine an application that supports organizations and the personal workspace, but includes both in their URLs. The might have URLs like:

URLIndicates
/orgs/bcorporg with slug "bcorp" should be active
/orgs/bcorp/settingsorg with slug "bcorp" should be active
/personal-workspace/homethe personal workspace should be active

Currently, that application would require special handling in both server and client javascript to detect and resolve an organization mismatch.

Now, they can add middleware config like this:

import{clerkMiddleware,createRouteMatcher}from"@clerk/nextjs/server";constisProtectedRoute=createRouteMatcher(["(.*)"]);exportdefaultclerkMiddleware((auth,req)=>{if(isProtectedRoute(req))auth().protect();},{organizationSyncOptions: {organizationPatterns: ["/orgs/:slug","/orgs/:slug/(.*)",],personalWorkspacePatterns: ["/personal-workspace","/personal-workspace/(.*)"],},});

What's next?

  • A public-facing guide explaining the new guidance for managing an organization via the URL
  • A public-facing sample repository demonstrating the new option

Further Reading:

Clerk internal DX guide: https://www.notion.so/clerkdev/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?pvs=4

@changeset-bot

changeset-botBot commented Aug 16, 2024

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc89068

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

This PR includes changesets to release 1 package
NameType
@clerk/nextjsPatch

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

Comment threadpackages/backend/src/tokens/request.ts Outdated
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch 2 times, most recently from cf8ad6c to 509ada1CompareSeptember 6, 2024 22:33
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 509ada1 to 54e6752CompareSeptember 6, 2024 22:47
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from cff9d48 to d724695CompareSeptember 6, 2024 23:12
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from d6b3f6b to 5c575eeCompareSeptember 9, 2024 15:32
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 42612d2 to aa84f66CompareSeptember 9, 2024 15:55
Comment threadintegration/tests/handshake.test.ts Outdated
await new Promise<void>(resolve => jwksServer.close(() => resolve()));
});

test('Test standard signed-in - dev', async () => {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

My IDE automatically deleted all of these Test prefixes, citing this:

ESLint: should not have duplicate prefix(playwright/valid-title)

I can add them back if we like them though!

izaaklauerand others added 7 commits September 27, 2024 10:53
Not sure why it made this change, but I believe it.
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@izaaklauer
izaaklauerforce-pushed the izaak/ORGS-132-middleware-orgsync branch from 9738a97 to 5ce428dCompareSeptember 27, 2024 18:57
Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment on lines +35 to +37
* WARNING: If the organization cannot be activated either because it does not exist or the user lacks access,
* organization-related fields will be set to null. The server component must detect this and respond
* with an appropriate error (e.g., notFound()).

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.

I think this is nice to have here, but it definitely needs to reach the customer facing documentation that we have as well.

Comment threadpackages/backend/src/tokens/types.ts Outdated
Comment on lines +49 to +60
organizationPatterns?: Array<Pattern>;

/**
* URL patterns for resources in the context of a clerk personal workspace (user-specific, outside any organization).
* If the route also matches the organizationPattern, this takes precedence.
*
* Common examples:
* - ["/user", "/user/(.*)"]
* - ["/user/:any", "/user/:any/(.*)"]
*/
personalWorkspacePatterns?: Array<Pattern>;
};

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.

Maybe there is a benefit of redefining those for the nextjs package and take advantage of our RouteMatcherWithNextTypedRoutes

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.

And just to double check here, we don't wanna use the createRouteMatcher pattern here, because we are not expecting for folks to use more than one path ? Although the name of the properties hints that you can. If not, I think supporting the route matcher here makes sense.

cc @nikosdouvlis

@izaaklauerizaaklauerOct 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good idea! I didn't realize we already have RouteMatcherParams, which serves the same purpose.

It looks like there's some duplication between the astro and nextjs RouteMatcherParams. My inclination here is to make the astro version (which doesn't depend on anything astro-specific) shared, and import that for use here. But let me know if you think another path would be better!

r/e createRouteMatcher - We could take a createRouteMatcher-returned function here too, but my inclination is to start with just accepting the pattern syntax for simplicity and expand the the more complex types if necessary in the future - I explored that a bit here (internal only): https://www.notion.so/Sync-Org-from-URL-to-Session-via-Middleware-f41f00865390480ab2279391c5625b27?d=f3738cb7f62c48bb90f8924c653a83a9&pvs=4#c0b57bf937694dd6bf510aeb4dde12af. Again, differing opinions welcome.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Oh you know, digging in a bit further, I think the main difference here between this and RouteMatcherWithNextTypedRoutes/RouteMatcherRoutes is that they're geared at returning true/false, not pulling information out of the path. For the organization pattern, we need it to include a group (which I have here as :slug or :id). I don't see a clear way to extend the RouteMatcherParams to encompass that without making the type too complex for my taste. I could have the personal workspace pattern use RouteMatcherParams, but I think the mixed types would be worse DX.

And to answer your question:

we are not expecting for folks to use more than one path ?

I am expecting more than one path! It's very tricky to get most practical examples down to just one path expression, so I think allowing multiples is the way to go.

Comment threadpackages/backend/src/tokens/request.ts Outdated
Comment threadpackages/backend/src/tokens/request.ts Outdated
});
});

test.describe('Client handshake with organization activation @nextjs', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing we've done in the past is pass options via request headers to avoid extra app overhead. Maybe this is something we can also do here?

Comment threadintegration/tests/handshake.test.ts Outdated
Comment threadpackages/backend/src/tokens/types.ts Outdated

@brkalowbrkalow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@izaaklauer
izaaklauer merged commit 2ea1a60 into mainOct 8, 2024
@izaaklauer
izaaklauer deleted the izaak/ORGS-132-middleware-orgsync branch October 8, 2024 13:35
wobsoriano pushed a commit that referenced this pull request Feb 8, 2025
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
@coderabbitaicoderabbitaiBot mentioned this pull request May 10, 2026
Sign up for freeto 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.

6 participants

@izaaklauer@brkalow@panteliselef@LauraBeatris@colinclerk@clerk-cookie