Skip to content
Open
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/tame-parents-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': patch
---

Fix `PUBLIC_CLERK_PUBLISHABLE_KEY` not readable from runtime environment when using the Astro Node adapter. Added `process.env` as a fallback in `getContextEnvVar()` for cases where `import.meta.env.PUBLIC_*` is statically replaced at build time by Vite.
133 changes: 133 additions & 0 deletions packages/astro/src/server/__tests__/get-safe-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { getClientSafeEnv, getSafeEnv } from '../get-safe-env';

function createLocals(overrides: Partial<App.Locals> = {}): App.Locals {
return {
runtime: { env: {} as InternalEnv },
...overrides,
} as unknown as App.Locals;
}

describe('getSafeEnv', () => {
beforeEach(() => {
vi.stubEnv('PUBLIC_CLERK_PUBLISHABLE_KEY', '');
vi.stubEnv('CLERK_SECRET_KEY', '');
});

afterEach(() => {
vi.unstubAllEnvs();
});

it('reads from locals.runtime.env first (Cloudflare)', () => {
const locals = createLocals({
runtime: {
env: {
PUBLIC_CLERK_PUBLISHABLE_KEY: 'pk_from_runtime',
CLERK_SECRET_KEY: 'sk_from_runtime',
} as InternalEnv,
},
});

// Also set process.env to verify runtime.env takes priority
process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process';
process.env.CLERK_SECRET_KEY = 'sk_from_process';

const env = getSafeEnv(locals);

expect(env.pk).toBe('pk_from_runtime');
expect(env.sk).toBe('sk_from_runtime');

delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY;
delete process.env.CLERK_SECRET_KEY;
});

it('reads from import.meta.env when runtime.env is not available', () => {
vi.stubEnv('PUBLIC_CLERK_PUBLISHABLE_KEY', 'pk_from_meta');
vi.stubEnv('CLERK_SECRET_KEY', 'sk_from_meta');

const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } });
const env = getSafeEnv(locals);

expect(env.pk).toBe('pk_from_meta');
expect(env.sk).toBe('sk_from_meta');
});

it('falls back to process.env when import.meta.env has no value', () => {
process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process';
process.env.CLERK_SECRET_KEY = 'sk_from_process';

const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } });
const env = getSafeEnv(locals);

expect(env.pk).toBe('pk_from_process');
expect(env.sk).toBe('sk_from_process');

delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY;
delete process.env.CLERK_SECRET_KEY;
});

it('returns undefined when no env source has the value', () => {
// Clean process.env so the fallback finds nothing
delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY;
delete process.env.CLERK_SECRET_KEY;

const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } });
const env = getSafeEnv(locals);

expect(env.pk).toBeUndefined();
expect(env.sk).toBeUndefined();
});

it('prefers keylessPublishableKey over all env sources', () => {
process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process';

const locals = createLocals({
runtime: { env: undefined as unknown as InternalEnv },
keylessPublishableKey: 'pk_keyless',
});
const env = getSafeEnv(locals);

expect(env.pk).toBe('pk_keyless');

delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY;
});
});

describe('getClientSafeEnv', () => {
beforeEach(() => {
vi.stubEnv('PUBLIC_CLERK_PUBLISHABLE_KEY', '');
});

afterEach(() => {
vi.unstubAllEnvs();
});

it('falls back to process.env for publishableKey', () => {
process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process';

const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } });
const env = getClientSafeEnv(locals);

expect(env.publishableKey).toBe('pk_from_process');

delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY;
});

it('falls back to process.env for all public env vars', () => {
process.env.PUBLIC_CLERK_DOMAIN = 'test.domain.com';
process.env.PUBLIC_CLERK_SIGN_IN_URL = '/sign-in';
process.env.PUBLIC_CLERK_SIGN_UP_URL = '/sign-up';

const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } });
const env = getClientSafeEnv(locals);

expect(env.domain).toBe('test.domain.com');
expect(env.signInUrl).toBe('/sign-in');
expect(env.signUpUrl).toBe('/sign-up');

delete process.env.PUBLIC_CLERK_DOMAIN;
delete process.env.PUBLIC_CLERK_SIGN_IN_URL;
delete process.env.PUBLIC_CLERK_SIGN_UP_URL;
});
});
13 changes: 12 additions & 1 deletion packages/astro/src/server/get-safe-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: Contex
return locals.runtime.env[envVarName];
}

return import.meta.env[envVarName];
const envValue = import.meta.env[envVarName];
if (envValue) {
return envValue;
}

// Fallback to process.env for runtime environments (e.g., Node.js adapter)
Copy link

@asdfjkalsdfla asdfjkalsdfla Feb 21, 2026

Choose a reason for hiding this comment

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

What do you think of having process.env have a higher precedence than import.meta.env?

My gut is that if someone defines this in their running environment that is what they intend vs. what they had it set to when they built an app. It would also make the runtime variables have higher priority like when deployed in Cloudflare Workers.

// where import.meta.env.PUBLIC_* is statically replaced at build time by Vite
if (typeof process !== 'undefined' && process.env) {
return process.env[envVarName];
}

return undefined;
}

/**
Expand Down
Loading