#7040·growthbook

[Feature] Split APP_ORIGIN into a CORS origin and a UI base URL

Author: guillem-aistechCreated Sep 18, 2026Updated Sep 18, 2026

Description of Feature

APP_ORIGIN (packages/back-end/src/util/secrets.ts:122-123) currently carries two different meanings that only coincide when the admin UI is served from a domain root:

1. A literal CORS origin. packages/back-end/src/app.ts:467 builds const origins = [APP_ORIGIN] and hands it to cors({ credentials: true, origin: origins }). A browser's Origin header is always scheme + host + port and never contains a path, so this only ever matches a bare origin.

2. A base URL for links back into the app. Roughly 19 sites across 10 files interpolate it as ${APP_ORIGIN}/some/path, including:

  • services/auth/OpenIdAuthConnection.ts:157,397 — OIDC redirect_uris
  • services/organizations.ts:621 — invitation URLs in emails
  • events/handlers/slack/slack-event-handler-utils.ts — 7 notification link builders
  • integrations/GoogleAnalytics.ts:70${APP_ORIGIN}/oauth/google
  • api/reports/toReportApiInterface.ts:37, services/experiments.ts:3166, enterprise/services/product-analytics.ts:359, routers/organizations/organizations.controller.ts:692,772

These want the full base URL, path included.

Nothing breaks today, because self-hosters set APP_ORIGIN to a bare origin and both readings agree. It becomes contradictory the moment the UI is not at a domain root — which is what #2176 asks for, and adjacent to #4020:

  • Set APP_ORIGIN=https://example.com/app/growthbook and the links and OIDC redirect URIs are right, but CORS matches nothing and every credentialed internal API call is blocked — the app is unusable, with no error pointing at the cause.
  • Set it bare and CORS works, but SSO redirect_uri registration, invitation emails and every Slack notification link point at a page that doesn't exist.

There is an escape hatch — set APP_ORIGIN with the path and CORS_ORIGIN_REGEX to the bare origin — but it's undocumented, nothing validates the pairing, and it reads as a misconfiguration to whoever inherits it. IS_LOCALHOST (secrets.ts:124) and the appOrigin reported to the license server also read the same variable.

Proposed change

Keep APP_ORIGIN as the CORS origin (no behaviour change, no migration), and add an optional APP_BASE_URL that defaults to APP_ORIGIN for the ~19 link-building sites. Existing deployments are unaffected; path-based ones get a coherent config instead of a regex workaround.

Worth doing independently of #2176 — it's a prerequisite for any implementation there, it lives in a different package, and it removes a footgun that currently fails with a blanket CORS rejection rather than anything diagnosable.