Skip to content

fix: add CLIENT_URL to CORS allowed origins for self-hosted deployments#859

Open
ramadanomar wants to merge 1 commit intouseautumn:devfrom
ramadanomar:fix/cors-client-url-self-hosted
Open

fix: add CLIENT_URL to CORS allowed origins for self-hosted deployments#859
ramadanomar wants to merge 1 commit intouseautumn:devfrom
ramadanomar:fix/cors-client-url-self-hosted

Conversation

@ramadanomar
Copy link

@ramadanomar ramadanomar commented Mar 2, 2026

Fixes #857

CLIENT_URL (and CHECKOUT_BASE_URL) were not included in the CORS allowlist or Better Auth's trustedOrigins in production, blocking self-hosted deployments on custom domains (e.g. Railway).

Changes:

  • corsOrigins.ts — allow CLIENT_URL and CHECKOUT_BASE_URL as valid origins unconditionally
  • auth.ts — move CLIENT_URL out of the dev-only guard so trustedOrigins includes it in production; add CHECKOUT_BASE_URL
  • corsOrigins.test.ts — add tests for self-hosted origin scenarios

Summary by cubic

Always allow CLIENT_URL and CHECKOUT_BASE_URL for self-hosted deployments by adding them to CORS and Better Auth trusted origins. Normalize env URLs to strict HTTP(S) origins so paths/trailing slashes match browser Origin headers.

  • Bug Fixes
    • CORS/Auth: normalize env URLs and allow CLIENT_URL/CHECKOUT_BASE_URL; reuse shared helpers across CORS, Better Auth trustedOrigins, and API auth middleware.
    • Tests: correct env restoration and add cases for whitespace, path/trailing slash, invalid/non-HTTP protocols, and unset envs; unrelated origins remain rejected.

Written for commit e4748ea. Summary will update on new commits.

Greptile Summary

This PR fixes CORS and Better Auth trustedOrigins misconfigurations that blocked self-hosted deployments (e.g. on Railway) from communicating with the server when using custom domains set via CLIENT_URL and CHECKOUT_BASE_URL.

Key Changes:

  • Bug fixescorsOrigins.ts: CLIENT_URL and CHECKOUT_BASE_URL are now checked unconditionally in isAllowedOrigin, allowing custom-domain self-hosted deployments through CORS in production.
  • Bug fixesauth.ts: CLIENT_URL is moved outside the NODE_ENV !== "production" guard so Better Auth's trustedOrigins includes it in production; CHECKOUT_BASE_URL is also added.
  • ImprovementscorsOrigins.test.ts: New self-hosted env URLs test suite covers production allowance, simultaneous use of both env vars, rejection of unrelated origins, and rejection when env vars are unset. The afterEach cleanup has a subtle issue where assigning undefined to a process.env key sets it to the string "undefined" rather than deleting it (see inline comment).

Confidence Score: 4/5

  • Safe to merge with a minor test-cleanup fix; the core CORS logic is correct and well-tested.
  • The production logic changes in corsOrigins.ts and auth.ts are straightforward and well-scoped: they add exact-match allowances for two operator-configured env vars. The only notable issues are (1) a test afterEach cleanup bug that doesn't break current tests but is semantically incorrect, and (2) a robustness gap if env vars include a path component. Neither blocks the fix from working in the typical deployment scenario.
  • server/tests/unit/corsOrigins.test.ts — afterEach cleanup incorrectly assigns undefined to process.env keys.

Important Files Changed

Filename Overview
server/src/utils/corsOrigins.ts Adds runtime checks for CLIENT_URL and CHECKOUT_BASE_URL in isAllowedOrigin; correct approach but exact-string comparison may fail if env vars include a path or trailing slash.
server/src/utils/auth.ts Moves CLIENT_URL and adds CHECKOUT_BASE_URL to trustedOrigins unconditionally; clean and correct change that unblocks self-hosted deployments in Better Auth.
server/tests/unit/corsOrigins.test.ts Good coverage of self-hosted scenarios, but afterEach cleanup incorrectly assigns undefined to process.env keys, which sets them to the string "undefined" rather than deleting them.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming request with Origin header] --> B{isAllowedOrigin}
    B --> C{In ALLOWED_ORIGINS hardcoded list?}
    C -- Yes --> ALLOW[Return origin ✅]
    C -- No --> D{CLIENT_URL set AND origin matches?}
    D -- Yes --> ALLOW
    D -- No --> E{CHECKOUT_BASE_URL set AND origin matches?}
    E -- Yes --> ALLOW
    E -- No --> F{NODE_ENV !== production AND localhost:port pattern?}
    F -- Yes --> ALLOW
    F -- No --> REJECT[Return undefined ❌]

    G[Better Auth trustedOrigins IIFE] --> H[Hardcoded: localhost:3000, app/staging/*.useautumn.com]
    H --> I{CLIENT_URL set?}
    I -- Yes --> J[Push CLIENT_URL]
    I -- No --> K{CHECKOUT_BASE_URL set?}
    J --> K
    K -- Yes --> L[Push CHECKOUT_BASE_URL]
    K -- No --> M{NODE_ENV !== production?}
    L --> M
    M -- Yes --> N[Push localhost:3000–3010]
    M -- No --> O[Return origins array]
    N --> O
Loading

Last reviewed commit: 6ba9b35

@vercel
Copy link

vercel bot commented Mar 2, 2026

@ramadanomar is attempting to deploy a commit to the Autumn Team on Vercel.

A member of the Team first needs to authorize it.

@ramadanomar ramadanomar force-pushed the fix/cors-client-url-self-hosted branch from cbcc3c5 to 6ba9b35 Compare March 2, 2026 13:24
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 9 to 13
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
process.env.CLIENT_URL = originalClientUrl;
process.env.CHECKOUT_BASE_URL = originalCheckoutBaseUrl;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

afterEach sets env vars to the string "undefined" instead of unsetting them

In Node.js/Bun, assigning undefined to a process.env key converts it to the string "undefined", rather than deleting the key. If CLIENT_URL or CHECKOUT_BASE_URL were unset in the environment before the test suite ran (the common case in CI), originalClientUrl and originalCheckoutBaseUrl will both be undefined. After each self-hosted test, afterEach will leave process.env.CLIENT_URL === "undefined" (a truthy string), causing any subsequent test that doesn't explicitly delete the key to see a non-empty CLIENT_URL.

The existing test ordering means this doesn't currently break anything, but it is incorrect cleanup and will silently corrupt state if tests are reordered or new tests are added. The cleanup should use delete when the original value was absent:

Suggested change
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
process.env.CLIENT_URL = originalClientUrl;
process.env.CHECKOUT_BASE_URL = originalCheckoutBaseUrl;
});
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
if (originalClientUrl === undefined) {
delete process.env.CLIENT_URL;
} else {
process.env.CLIENT_URL = originalClientUrl;
}
if (originalCheckoutBaseUrl === undefined) {
delete process.env.CHECKOUT_BASE_URL;
} else {
process.env.CHECKOUT_BASE_URL = originalCheckoutBaseUrl;
}
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/tests/unit/corsOrigins.test.ts
Line: 9-13

Comment:
**`afterEach` sets env vars to the string `"undefined"` instead of unsetting them**

In Node.js/Bun, assigning `undefined` to a `process.env` key converts it to the **string** `"undefined"`, rather than deleting the key. If `CLIENT_URL` or `CHECKOUT_BASE_URL` were unset in the environment before the test suite ran (the common case in CI), `originalClientUrl` and `originalCheckoutBaseUrl` will both be `undefined`. After each self-hosted test, `afterEach` will leave `process.env.CLIENT_URL === "undefined"` (a truthy string), causing any subsequent test that doesn't explicitly `delete` the key to see a non-empty `CLIENT_URL`.

The existing test ordering means this doesn't currently break anything, but it is incorrect cleanup and will silently corrupt state if tests are reordered or new tests are added. The cleanup should use `delete` when the original value was absent:

```suggestion
	afterEach(() => {
		process.env.NODE_ENV = originalNodeEnv;
		if (originalClientUrl === undefined) {
			delete process.env.CLIENT_URL;
		} else {
			process.env.CLIENT_URL = originalClientUrl;
		}
		if (originalCheckoutBaseUrl === undefined) {
			delete process.env.CHECKOUT_BASE_URL;
		} else {
			process.env.CHECKOUT_BASE_URL = originalCheckoutBaseUrl;
		}
	});
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Author

Choose a reason for hiding this comment

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

Pre-existing pattern — the original test already does process.env.NODE_ENV = originalNodeEnv the same way. Happy to fix all three in this PR if maintainers prefer, but wanted to keep the diff focused.

Comment on lines +23 to +32
// Allow CLIENT_URL and CHECKOUT_BASE_URL for self-hosted deployments
if (process.env.CLIENT_URL && origin === process.env.CLIENT_URL) {
return origin;
}
if (
process.env.CHECKOUT_BASE_URL &&
origin === process.env.CHECKOUT_BASE_URL
) {
return origin;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider normalizing env var URLs to their origin before comparing

Browsers always send scheme://host[:port] — no path — in the Origin header. If CLIENT_URL or CHECKOUT_BASE_URL is configured with a path component (e.g. https://checkout.example.com/app), the exact string comparison will never match the browser's origin (https://checkout.example.com) and CORS will silently fail for that deployment.

Extracting just the origin makes the check robust against trailing slashes and paths:

// Allow CLIENT_URL and CHECKOUT_BASE_URL for self-hosted deployments
const clientOrigin = process.env.CLIENT_URL
    ? new URL(process.env.CLIENT_URL).origin
    : null;
const checkoutOrigin = process.env.CHECKOUT_BASE_URL
    ? new URL(process.env.CHECKOUT_BASE_URL).origin
    : null;

if (clientOrigin && origin === clientOrigin) return origin;
if (checkoutOrigin && origin === checkoutOrigin) return origin;

The same normalisation should be applied in auth.ts before origins.push(...).

Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/utils/corsOrigins.ts
Line: 23-32

Comment:
**Consider normalizing env var URLs to their origin before comparing**

Browsers always send `scheme://host[:port]` — no path — in the `Origin` header. If `CLIENT_URL` or `CHECKOUT_BASE_URL` is configured with a path component (e.g. `https://checkout.example.com/app`), the exact string comparison will never match the browser's origin (`https://checkout.example.com`) and CORS will silently fail for that deployment.

Extracting just the origin makes the check robust against trailing slashes and paths:

```ts
// Allow CLIENT_URL and CHECKOUT_BASE_URL for self-hosted deployments
const clientOrigin = process.env.CLIENT_URL
    ? new URL(process.env.CLIENT_URL).origin
    : null;
const checkoutOrigin = process.env.CHECKOUT_BASE_URL
    ? new URL(process.env.CHECKOUT_BASE_URL).origin
    : null;

if (clientOrigin && origin === clientOrigin) return origin;
if (checkoutOrigin && origin === checkoutOrigin) return origin;
```

The same normalisation should be applied in `auth.ts` before `origins.push(...)`.

How can I resolve this? If you propose a fix, please make it concise.

Normalize CLIENT_URL and CHECKOUT_BASE_URL to strict HTTP(S) origins so browser Origin headers match even when env values include paths or trailing slashes. Reuse shared dashboard-origin checks in API auth and add focused unit coverage for malformed URL and env-restore edge cases.
@ramadanomar ramadanomar force-pushed the fix/cors-client-url-self-hosted branch from 8f6e89c to e4748ea Compare March 3, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CORS blocks self-hosted deployments — CLIENT_URL not in allowed origins

1 participant