[Feature] Split APP_ORIGIN into a CORS origin and a UI base URL
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— OIDCredirect_urisservices/organizations.ts:621— invitation URLs in emailsevents/handlers/slack/slack-event-handler-utils.ts— 7 notification link buildersintegrations/GoogleAnalytics.ts:70—${APP_ORIGIN}/oauth/googleapi/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/growthbookand 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_uriregistration, 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.
Source: growthbook/growthbook