Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-keys-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/react': minor
---

Add automatic environment variable fallback for Vite applications. When `publishableKey` is not explicitly provided to `ClerkProvider`, the SDK now checks for `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_PUBLISHABLE_KEY` environment variables.
8 changes: 0 additions & 8 deletions integration/templates/custom-flows-react-vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@ import { SignUp } from './routes/SignUp';
import { Protected } from './routes/Protected';
import { Waitlist } from './routes/Waitlist';

// Import your Publishable Key
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;

if (!PUBLISHABLE_KEY) {
throw new Error('Add your Clerk Publishable Key to the .env file');
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
<div className='bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10'>
<div className='flex w-full max-w-sm flex-col gap-6'>
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string}
clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string}
appearance={{
Expand Down
2 changes: 0 additions & 2 deletions integration/templates/react-vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ const Root = () => {
const navigate = useNavigate();
return (
<ClerkProvider
// @ts-ignore
publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string}
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string}
clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string}
routerPush={(to: string) => navigate(to)}
Expand Down
17 changes: 10 additions & 7 deletions packages/react/src/contexts/ClerkContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React from 'react';

import { IsomorphicClerk } from '../isomorphicClerk';
import type { IsomorphicClerkOptions } from '../types';
import { mergeWithEnv } from '../utils';
import { AuthContext } from './AuthContext';
import { IsomorphicClerkContext } from './IsomorphicClerkContext';

Expand All @@ -24,7 +25,9 @@ export type ClerkContextProviderState = Resources;

export function ClerkContextProvider(props: ClerkContextProvider) {
const { isomorphicClerkOptions, initialState, children } = props;
const { isomorphicClerk: clerk, clerkStatus } = useLoadedIsomorphicClerk(isomorphicClerkOptions);
// Merge options with environment variable fallbacks (supports Vite's VITE_CLERK_* env vars)
const mergedOptions = mergeWithEnv(isomorphicClerkOptions);
const { isomorphicClerk: clerk, clerkStatus } = useLoadedIsomorphicClerk(mergedOptions);

const [state, setState] = React.useState<ClerkContextProviderState>({
client: clerk.client as ClientResource,
Expand Down Expand Up @@ -111,17 +114,17 @@ export function ClerkContextProvider(props: ClerkContextProvider) {
);
}

const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => {
const isomorphicClerkRef = React.useRef(IsomorphicClerk.getOrCreateInstance(options));
const useLoadedIsomorphicClerk = (mergedOptions: IsomorphicClerkOptions) => {
const isomorphicClerkRef = React.useRef(IsomorphicClerk.getOrCreateInstance(mergedOptions));
const [clerkStatus, setClerkStatus] = React.useState(isomorphicClerkRef.current.status);

React.useEffect(() => {
void isomorphicClerkRef.current.__internal_updateProps({ appearance: options.appearance });
}, [options.appearance]);
void isomorphicClerkRef.current.__internal_updateProps({ appearance: mergedOptions.appearance });
}, [mergedOptions.appearance]);

React.useEffect(() => {
void isomorphicClerkRef.current.__internal_updateProps({ options });
}, [options.localization]);
void isomorphicClerkRef.current.__internal_updateProps({ options: mergedOptions });
}, [mergedOptions.localization]);

React.useEffect(() => {
isomorphicClerkRef.current.on('status', setClerkStatus);
Expand Down
110 changes: 110 additions & 0 deletions packages/react/src/utils/__tests__/envVariables.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as getEnvVariableModule from '@clerk/shared/getEnvVariable';
import type { IsomorphicClerkOptions } from '@clerk/shared/types';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { mergeWithEnv } from '../envVariables';

// Mock getEnvVariable to control env var behavior in tests
vi.mock('@clerk/shared/getEnvVariable', () => ({
getEnvVariable: vi.fn(() => ''),
}));

describe('mergeWithEnv', () => {
const mockedGetEnvVariable = vi.mocked(getEnvVariableModule.getEnvVariable);

beforeEach(() => {
vi.clearAllMocks();
});

it('returns passed-in publishableKey when provided', () => {
mockedGetEnvVariable.mockReturnValue('should_not_be_used');

const options: IsomorphicClerkOptions = {
publishableKey: 'pk_test_explicit',
};

const result = mergeWithEnv(options);

expect(result.publishableKey).toBe('pk_test_explicit');
});

it('falls back to VITE_CLERK_PUBLISHABLE_KEY env var when option is undefined', () => {
mockedGetEnvVariable.mockImplementation((name: string) => {
if (name === 'VITE_CLERK_PUBLISHABLE_KEY') {
return 'pk_test_vite';
}
return '';
});

const result = mergeWithEnv({} as any);

expect(result.publishableKey).toBe('pk_test_vite');
});

it('falls back to CLERK_PUBLISHABLE_KEY when VITE_ prefixed not set', () => {
mockedGetEnvVariable.mockImplementation((name: string) => {
if (name === 'CLERK_PUBLISHABLE_KEY') {
return 'pk_test_node';
}
return '';
});

const result = mergeWithEnv({} as any);

expect(result.publishableKey).toBe('pk_test_node');
});

it('prioritizes VITE_ prefixed env var over non-prefixed', () => {
mockedGetEnvVariable.mockImplementation((name: string) => {
const envVars: Record<string, string> = {
VITE_CLERK_PUBLISHABLE_KEY: 'pk_test_vite',
CLERK_PUBLISHABLE_KEY: 'pk_test_node',
};
return envVars[name] || '';
});

const result = mergeWithEnv({} as any);

expect(result.publishableKey).toBe('pk_test_vite');
});

it('does NOT fall back when publishableKey is empty string (framework SDK behavior)', () => {
mockedGetEnvVariable.mockReturnValue('pk_test_vite');

const result = mergeWithEnv({
publishableKey: '',
});

// Should preserve empty string, not fall back to env var
expect(result.publishableKey).toBe('');
});

it('returns undefined publishableKey when neither option nor env var is set', () => {
mockedGetEnvVariable.mockReturnValue('');

const result = mergeWithEnv({} as any);

// When env var is not set, we don't add the property
expect(result.publishableKey).toBeUndefined();
});

it('preserves other options that are not env-var backed', () => {
mockedGetEnvVariable.mockReturnValue('');

const options: IsomorphicClerkOptions = {
publishableKey: 'pk_test',
appearance: { variables: { colorPrimary: 'red' } },
localization: { signIn: { start: { title: 'Hello' } } },
signInUrl: '/custom-sign-in',
signUpUrl: '/custom-sign-up',
};

const result = mergeWithEnv(options);

expect(result.publishableKey).toBe('pk_test');
expect(result.appearance).toEqual({ variables: { colorPrimary: 'red' } });
expect(result.localization).toEqual({ signIn: { start: { title: 'Hello' } } });
expect(result.signInUrl).toBe('/custom-sign-in');
expect(result.signUpUrl).toBe('/custom-sign-up');
});
});
61 changes: 61 additions & 0 deletions packages/react/src/utils/envVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getEnvVariable } from '@clerk/shared/getEnvVariable';

import type { IsomorphicClerkOptions } from '../types';

/**
* Gets an environment variable value, checking for Vite's VITE_ prefix first.
* This allows React SDK users with Vite to use VITE_CLERK_* env vars
* (which Vite exposes client-side) without manual configuration.
*
* Note: Empty string values are treated as "not set" and will fall through to
* the next env var in the chain. This is intentional since empty values are
* typically invalid for these options.
*
* @param name - The environment variable name without prefix (e.g., 'CLERK_PUBLISHABLE_KEY')
* @returns The value of the environment variable, or empty string if not found
*/
const getEnvVar = (name: string): string => {
// Check for Vite-prefixed env var first (client-side exposed)
// Then fall back to unprefixed version (for SSR, Node.js, etc.)
// Note: Uses || so empty string falls through to the next check
return getEnvVariable(`VITE_${name}`) || getEnvVariable(name);
};

/**
* Helper to get env fallback only when the option is undefined.
* We check for undefined specifically (not falsy) to avoid conflicting with framework SDKs
* that may pass an empty string when their env var is not set.
*
* Returns the env var value only if it's non-empty, otherwise returns undefined
* to preserve the original behavior when no env var is set.
*/
const withEnvFallback = (value: string | undefined, envVarName: string): string | undefined => {
if (value !== undefined) {
return value;
}
const envValue = getEnvVar(envVarName);
return envValue || undefined;
};

/**
* Merges ClerkProvider options with environment variable fallbacks.
* This supports Vite users who set VITE_CLERK_* or CLERK_* env vars.
* Passed-in options always take priority over environment variables.
*
* Supported environment variables:
* - VITE_CLERK_PUBLISHABLE_KEY / CLERK_PUBLISHABLE_KEY
*
* @param options - The options passed to ClerkProvider
* @returns Options with environment variable fallbacks applied
*/
export const mergeWithEnv = (options: IsomorphicClerkOptions): IsomorphicClerkOptions => {
// Get env fallback values (undefined if not set)
const publishableKey = withEnvFallback(options.publishableKey, 'CLERK_PUBLISHABLE_KEY');

// Only add publishableKey to result if it has a defined value
// URL fallbacks removed due to compatibility issues with @clerk/react-router
return {
...options,
...(publishableKey !== undefined && { publishableKey }),
};
};
1 change: 1 addition & 0 deletions packages/react/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './childrenUtils';
export * from './envVariables';
export * from './isConstructor';
export * from './useMaxAllowedInstancesGuard';
export * from './useCustomElementPortal';
Expand Down
Loading