diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json
index a41a1197d97..77f74580d60 100644
--- a/packages/clerk-js/package.json
+++ b/packages/clerk-js/package.json
@@ -45,6 +45,26 @@
"types": "./dist/types/index.d.ts",
"default": "./dist/clerk.no-rhc.js"
}
+ },
+ "./bundled": {
+ "react-server": {
+ "types": "./dist/esm/server.d.mts",
+ "import": "./dist/esm/server.mjs",
+ "default": "./dist/esm/server.mjs"
+ },
+ "types": "./dist/esm/bundled.d.mts",
+ "import": "./dist/esm/bundled.mjs",
+ "default": "./dist/esm/bundled.mjs"
+ },
+ "./entry": {
+ "types": "./dist/esm/entry.d.mts",
+ "import": "./dist/esm/entry.mjs",
+ "default": "./dist/esm/entry.mjs"
+ },
+ "./internal": {
+ "types": "./dist/esm/internal/index.d.mts",
+ "import": "./dist/esm/internal/index.mjs",
+ "default": "./dist/esm/internal/index.mjs"
}
},
"main": "dist/clerk.js",
@@ -56,9 +76,10 @@
"no-rhc"
],
"scripts": {
- "build": "pnpm build:bundle && pnpm build:declarations",
+ "build": "pnpm build:bundle && pnpm build:esm && pnpm build:declarations",
"build:analyze": "rspack build --config rspack.config.js --env production --env variant=\"clerk.browser\" --env analysis --analyze",
"build:bundle": "pnpm clean && rspack build --config rspack.config.js --env production",
+ "build:esm": "tsdown",
"build:declarations": "tsc -p tsconfig.declarations.json",
"build:sandbox": "pnpm --filter @clerk/ui build:umd && rspack build --config rspack.config.js --env production --env sandbox",
"build:stats": "rspack build --config rspack.config.js --env production --json=stats.json --env variant=\"clerk.browser\"",
@@ -114,6 +135,7 @@
"bundlewatch": "^0.4.1",
"jsdom": "26.1.0",
"minimatch": "^10.0.3",
+ "tsdown": "catalog:repo",
"webpack-merge": "^5.10.0"
},
"engines": {
diff --git a/packages/clerk-js/src/bundled.ts b/packages/clerk-js/src/bundled.ts
new file mode 100644
index 00000000000..aa2d52e7787
--- /dev/null
+++ b/packages/clerk-js/src/bundled.ts
@@ -0,0 +1,27 @@
+import type { Js } from './internal';
+import { JS_BRAND } from './internal';
+
+import { Clerk } from './core/clerk';
+
+declare const PACKAGE_VERSION: string;
+
+/**
+ * JS object for bundled Clerk JS.
+ * Pass this to ClerkProvider to use the bundled clerk-js instead of loading from CDN.
+ *
+ * @example
+ * ```tsx
+ * import { js } from '@clerk/clerk-js/bundled';
+ *
+ *
+ * ...
+ *
+ * ```
+ */
+export const js = {
+ __brand: JS_BRAND,
+ version: PACKAGE_VERSION,
+ ClerkJS: Clerk,
+} as unknown as Js;
+
+export type { Js } from './internal';
diff --git a/packages/clerk-js/src/entry.ts b/packages/clerk-js/src/entry.ts
new file mode 100644
index 00000000000..d159251e99a
--- /dev/null
+++ b/packages/clerk-js/src/entry.ts
@@ -0,0 +1,5 @@
+/**
+ * Entry point for dynamic import of ClerkJS constructor.
+ * Used by the SDK when the js prop is a server-safe marker (without ClerkJS constructor).
+ */
+export { Clerk as ClerkJs } from './core/clerk';
diff --git a/packages/clerk-js/src/internal/index.ts b/packages/clerk-js/src/internal/index.ts
new file mode 100644
index 00000000000..fcc76912c0c
--- /dev/null
+++ b/packages/clerk-js/src/internal/index.ts
@@ -0,0 +1,35 @@
+import type { BrowserClerkConstructor } from '@clerk/shared/types';
+
+declare const Tags: unique symbol;
+type Tagged = BaseType & { [Tags]: { [K in Tag]: void } };
+
+/**
+ * Runtime brand value to identify valid JS objects
+ */
+export const JS_BRAND = '__clerkJS' as const;
+
+/**
+ * Js type that carries type information via phantom property
+ * Tagged to ensure only official js objects from @clerk/clerk-js can be used
+ *
+ * ClerkJS is optional to support server-safe marker exports (react-server condition).
+ * When ClerkJS is absent, the SDK will dynamically import it.
+ */
+export type Js = Tagged<
+ {
+ /**
+ * Runtime brand to identify valid JS objects
+ */
+ __brand: typeof JS_BRAND;
+ /**
+ * ClerkJS constructor. Optional to support server-safe marker exports.
+ * When absent (e.g., in React Server Components), the SDK resolves it via dynamic import.
+ */
+ ClerkJS?: BrowserClerkConstructor;
+ /**
+ * Version of the JS package (for potential future use)
+ */
+ version?: string;
+ },
+ 'ClerkJs'
+>;
diff --git a/packages/clerk-js/src/server.ts b/packages/clerk-js/src/server.ts
new file mode 100644
index 00000000000..af80aa69157
--- /dev/null
+++ b/packages/clerk-js/src/server.ts
@@ -0,0 +1,28 @@
+import type { Js } from './internal';
+import { JS_BRAND } from './internal';
+
+declare const PACKAGE_VERSION: string;
+
+/**
+ * Server-safe JS marker for React Server Components.
+ *
+ * This export does not include the ClerkJS constructor, making it safe to import
+ * in server components. The constructor is resolved via dynamic import when needed.
+ *
+ * @example
+ * ```tsx
+ * // app/layout.tsx (server component)
+ * import { ClerkProvider } from '@clerk/nextjs';
+ * import { js } from '@clerk/clerk-js/bundled';
+ *
+ * export default function Layout({ children }) {
+ * return {children};
+ * }
+ * ```
+ */
+export const js = {
+ __brand: JS_BRAND,
+ version: PACKAGE_VERSION,
+} as unknown as Js;
+
+export type { Js } from './internal';
diff --git a/packages/clerk-js/tsdown.config.mts b/packages/clerk-js/tsdown.config.mts
new file mode 100644
index 00000000000..de221c336be
--- /dev/null
+++ b/packages/clerk-js/tsdown.config.mts
@@ -0,0 +1,35 @@
+import type { Options } from 'tsdown';
+import { defineConfig } from 'tsdown';
+
+import clerkJsPackage from './package.json' with { type: 'json' };
+
+export default defineConfig(({ watch }) => {
+ const common = {
+ dts: true,
+ sourcemap: true,
+ clean: false,
+ target: 'es2022',
+ platform: 'browser',
+ external: ['@clerk/shared'],
+ format: ['esm'],
+ minify: false,
+ define: {
+ PACKAGE_NAME: `"${clerkJsPackage.name}"`,
+ PACKAGE_VERSION: `"${clerkJsPackage.version}"`,
+ __PKG_VERSION__: `"${clerkJsPackage.version}"`,
+ __DEV__: `${watch}`,
+ __BUILD_DISABLE_RHC__: JSON.stringify(false),
+ },
+ } satisfies Options;
+
+ return [
+ // Build internal types, server (marker), entry point, and bundled wrapper
+ // These are unbundled - the user's bundler handles dependency resolution
+ {
+ ...common,
+ entry: ['./src/internal/index.ts', './src/server.ts', './src/entry.ts', './src/bundled.ts'],
+ outDir: './dist/esm',
+ unbundle: true,
+ },
+ ];
+});
diff --git a/packages/react/package.json b/packages/react/package.json
index 057e59ba733..4ebe16e15e9 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -107,6 +107,7 @@
"tslib": "catalog:repo"
},
"devDependencies": {
+ "@clerk/clerk-js": "workspace:*",
"@clerk/localizations": "workspace:*",
"@clerk/ui": "workspace:*",
"@types/semver": "^7.7.1",
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index e42d315677f..32942367129 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -76,6 +76,7 @@ import type {
HeadlessBrowserClerkConstructor,
IsomorphicClerkOptions,
} from './types';
+import { JS_BRAND } from './types';
import { isConstructor } from './utils';
if (typeof globalThis.__BUILD_DISABLE_RHC__ === 'undefined') {
@@ -91,6 +92,7 @@ const SDK_METADATA = {
export interface Global {
Clerk?: HeadlessBrowserClerk | BrowserClerk;
__internal_ClerkUICtor?: ClerkUiConstructor;
+ __internal_ClerkJSCtor?: BrowserClerkConstructor;
}
declare const global: Global;
@@ -487,10 +489,32 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}
private async getClerkJsEntryChunk(): Promise {
- // Hotload bundle
- if (!this.options.Clerk && !__BUILD_DISABLE_RHC__) {
+ // Support bundled JS via js.ClerkJS prop
+ const jsProp = (this.options as { js?: { __brand?: string; ClerkJS?: BrowserClerkConstructor } }).js;
+ if (jsProp?.ClerkJS) {
+ global.Clerk = new jsProp.ClerkJS(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain });
+ return global.Clerk;
+ }
+
+ // Support server-safe JS marker (react-server condition)
+ // When js prop is present but ClerkJS is absent, dynamically import
+ if (jsProp?.__brand === JS_BRAND) {
+ const { ClerkJs } = await import('@clerk/clerk-js/entry');
+ global.Clerk = new ClerkJs(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain });
+ return global.Clerk;
+ }
+
+ // Legacy: Support bundled Clerk via Clerk prop
+ if (this.options.Clerk) {
+ global.Clerk = isConstructor(this.options.Clerk)
+ ? new this.options.Clerk(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain })
+ : this.options.Clerk;
+ return global.Clerk;
+ }
+
+ // Hotload bundle from CDN
+ if (!__BUILD_DISABLE_RHC__) {
// the UMD script sets the global.Clerk instance
- // we do not want to await here as we
await loadClerkJSScript({
...this.options,
publishableKey: this.#publishableKey,
@@ -500,13 +524,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
});
}
- // Otherwise, set global.Clerk to the bundled ctor or instance
- if (this.options.Clerk) {
- global.Clerk = isConstructor(this.options.Clerk)
- ? new this.options.Clerk(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain })
- : this.options.Clerk;
- }
-
if (!global.Clerk) {
// TODO @nikos: somehow throw if clerk ui failed to load but it was not headless
throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.');
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index 96c7e7a0b53..58293b6975b 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -1,4 +1,5 @@
import type {
+ BrowserClerkConstructor,
Clerk,
InitialState,
IsomorphicClerkOptions,
@@ -12,6 +13,40 @@ import type { ClerkUiConstructor } from '@clerk/shared/ui';
import type { Appearance, ExtractAppearanceType, Ui } from '@clerk/ui/internal';
import type React from 'react';
+/**
+ * Runtime brand value to identify valid JS objects
+ */
+export const JS_BRAND = '__clerkJS' as const;
+
+declare const Tags: unique symbol;
+type Tagged = BaseType & { [Tags]: { [K in Tag]: void } };
+
+/**
+ * Js type that carries type information via phantom property
+ * Tagged to ensure only official js objects from @clerk/clerk-js can be used
+ *
+ * ClerkJS is optional to support server-safe marker exports (react-server condition).
+ * When ClerkJS is absent, the SDK will dynamically import it.
+ */
+export type Js = Tagged<
+ {
+ /**
+ * Runtime brand to identify valid JS objects
+ */
+ __brand: typeof JS_BRAND;
+ /**
+ * ClerkJS constructor. Optional to support server-safe marker exports.
+ * When absent (e.g., in React Server Components), the SDK resolves it via dynamic import.
+ */
+ ClerkJS?: BrowserClerkConstructor;
+ /**
+ * Version of the JS package (for potential future use)
+ */
+ version?: string;
+ },
+ 'ClerkJs'
+>;
+
// Re-export types from @clerk/shared that are used by other modules
export type {
IsomorphicClerkOptions,
@@ -30,13 +65,14 @@ declare global {
__clerk_proxy_url?: Clerk['proxyUrl'];
__clerk_domain?: Clerk['domain'];
__internal_ClerkUICtor?: ClerkUiConstructor;
+ __internal_ClerkJSCtor?: BrowserClerkConstructor;
}
}
/**
* @interface
*/
-export type ClerkProviderProps = Omit & {
+export type ClerkProviderProps = Omit & {
children: React.ReactNode;
/**
* Provide an initial state of the Clerk client during server-side rendering. You don't need to set this value yourself unless you're [developing an SDK](https://clerk.com/docs/guides/development/sdk-development/overview).
@@ -59,6 +95,12 @@ export type ClerkProviderProps = Omit = T & {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index adb035f97a6..7ce8bfd24e3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -544,6 +544,9 @@ importers:
minimatch:
specifier: ^10.0.3
version: 10.1.1
+ tsdown:
+ specifier: catalog:repo
+ version: 0.15.7(publint@0.3.15)(typescript@5.8.3)(vue-tsc@3.1.4(typescript@5.8.3))
webpack-merge:
specifier: ^5.10.0
version: 5.10.0
@@ -791,6 +794,9 @@ importers:
specifier: catalog:repo
version: 2.8.1
devDependencies:
+ '@clerk/clerk-js':
+ specifier: workspace:*
+ version: link:../clerk-js
'@clerk/localizations':
specifier: workspace:*
version: link:../localizations