Why teams serve the Backstage frontend separately with NGINX
A platform team running Backstage as one process usually finds that the same server answers API calls, plugin routes and the static JavaScript bundle that browsers download on each visit. The decision is whether to keep that arrangement or to move the frontend onto NGINX or a CDN while the backend keeps serving the APIs. The split changes what the backend is responsible for. The frontend becomes a set of static files built ahead of time, and the backend handles only the API traffic the browser sends. Before you change anything, confirm in the architecture overview how the frontend and backend are separated in the release line you run, because the plugin wiring is what you will be altering.
Splitting is worth the effort when you want the bundle cached at the edge, when frontend releases should not require a backend restart, or when you need header and TLS control at a layer that already handles your other web traffic. The built-in app-backend plugin is usually enough for a single internal instance with modest traffic, one ingress and a team that deploys both halves together. The split adds a second deployable, a second set of build outputs and a configuration path to keep aligned. Plan for that cost explicitly. The backend no longer injects frontend configuration at runtime, so the values the browser needs, such as the backend base URL and the app's public origin, must come from the build or from files the static host serves.
Choosing between app-backend, NGINX and a CDN
This table compares keeping the frontend inside app-backend, serving the static bundle from a self-managed NGINX container, and publishing it from a CDN or object storage origin. Read each row as a decision question: what the option fits, what it changes for configuration and cache invalidation, and who carries the operating work. The main takeaway is that moving the frontend out of the backend is mainly a change to how runtime configuration reaches the browser and how releases invalidate caches.
| Decision area | Keep app-backend | Self-managed NGINX container | CDN or object storage origin |
|---|---|---|---|
| Best fit | Single deployment, small team, and no need to scale static delivery apart from the backend | Teams that run containers already and want the frontend on the same cluster and ingress as the backend | Organizations with users spread across regions or heavy static traffic that benefit from edge caching |
| Runtime configuration | Backend injects frontend configuration at serve time, so no separate config step is needed | A config file or entrypoint script must write runtime values into the bundle or a served JSON file at container start | Config must be published as a separate object or generated at build, and changes need a deploy or object update |
| Cache invalidation | Changes follow backend releases, and the backend controls caching headers for its own responses | Hashed asset filenames allow long cache lifetimes, while the HTML and config files need short or no caching | Requires a purge or invalidation step for HTML and config paths after each release, and the process must be tested |
| Routing and CORS | Same origin as the API through the backend, which avoids most cross-origin setup | Ingress routes static paths to NGINX and API paths to the backend, so origin and path rules must match the frontend's base URL | Frontend and API usually live on different origins, so CORS, cookie scope and redirect URIs need explicit design |
| Security headers | Set by the backend, which must be reviewed for the frontend's content policy | Set in the NGINX configuration, which the team owns and must test on every change | Set by the CDN or storage layer, which needs its own configuration review and may differ by provider |
| Who operates it | Platform team operates the backend process only | Platform team operates the NGINX image, its patching, ingress and logs | Platform or network team operates the CDN account, certificates, invalidation and origin access policy |
| What to confirm | Backend capacity for static traffic at expected peaks | Image patch process and the NGINX version your base image ships | Provider's invalidation limits, log access and whether your data-residency terms allow edge caching |
Steps to remove app-backend and serve the bundle
This sequence moves the Backstage frontend out of the backend process and onto a static web server or CDN, while keeping the API, sign-in and deep links working. Each step ends with a check so the platform team can confirm the split before moving on.
Inventory origins and configuration keys
The platform engineer lists the public frontend URL, the public backend URL, every OAuth or OIDC callback path, and each app-config key that references either origin, such as app.baseUrl and backend.baseUrl. Identity provider administrators confirm which redirect URIs they can change and when. The step is done when the team has one written table of origins and keys that every later step uses. (Illustrative: a frontend at a portal hostname and an API at a separate hostname.)
Build the frontend bundle separately
The frontend developer builds the app package on its own with the production build command, producing a static output directory. The build should run with the same app-config values the target environment will use, because the backend will no longer supply them at runtime. The check is that the output directory contains index.html and hashed asset files, and that a local static file server can open the app shell without the backend running.
Remove the app-backend plugin from the backend
The backend developer removes the app-backend package from the backend's dependencies, deletes its registration line in the backend entry point, and rebuilds the backend. Once the plugin is gone, the backend stops serving the frontend. The check is that the backend starts cleanly, its API routes still respond, and a request for the frontend path on the backend origin no longer returns the Backstage app shell.
Configure NGINX with SPA fallback
The operations engineer points an NGINX server block (or CDN origin rules) at the static output directory and adds a fallback so unknown paths return index.html, which lets the client-side router handle deep links. Hashed assets get long cache lifetimes, while index.html is served with no-cache so new releases load immediately. Requests under the API path are proxied to the backend origin. The check is that a direct request to a catalog or template route returns the app shell with a 200 status.
Set frontend runtime configuration
Because the backend no longer injects frontend configuration, the platform team sets the public app and backend base URLs in the frontend build or in a configuration file served alongside the bundle. Confirm the mechanism that your Backstage version supports in its current documentation before relying on it. The check is that the browser's network activity shows API calls going to the backend origin, and the console reports no configuration or missing-origin errors on first load.
Update CORS, cookies and callback URLs
The backend developer sets backend.cors.origin to the frontend origin and confirms that cookie attributes allow the browser to send credentials when the frontend and API run on different sites. The identity administrator registers the backend callback URL with each provider, and removes any redirect URI that pointed at the old combined host. Review the Backstage authentication documentation for provider-specific settings. The check is that a preflight request from the frontend origin returns the expected allow-origin header.
Verify sign-in and deep links end to end
The QA owner runs a written test list in each environment: sign in with every enabled provider, confirm guest access is off in production, open a catalog entity, a template and a TechDocs page directly, refresh each page, and use the browser back button. Sign-out and session expiry are tested too. The step is done when every case passes and the results are recorded with the build and configuration versions, so the next upgrade can repeat the same checks.
How frontend configuration changes when the backend stops injecting it
Once the app-backend plugin stops serving the frontend, nothing injects the app section of your configuration into the page, so the static bundle must get its runtime settings from somewhere else before the first request to the backend. The values that matter most are app.baseUrl, backend.baseUrl, and the sign-in provider settings the browser uses to start authentication. Your first decision is when each value is fixed: when the bundle is compiled, when NGINX serves it, or when the container starts. That choice determines how many artifacts you build per environment and what a single configuration change costs in time and release effort.
Build-time values are the simplest option. Settings from your production configuration are compiled into the JavaScript during the frontend build, so NGINX serves files that need no further work. The cost is that each environment needs its own bundle, and any hostname change means a rebuild and redeploy. A served config file moves those values into a small JSON or JavaScript file that NGINX returns beside the bundle, so one build can run in several environments. The trade-off is that the frontend must load that file at startup, so confirm how your Backstage version expects external configuration to be loaded before you design around it. Environment substitution at container start sits between the two: an entrypoint script renders a template with envsubst or a similar tool, which keeps one image portable across environments. Each option suits only non-secret values, because anything the browser loads is visible to every user.
Whichever option you choose, treat a change to frontend configuration as a release. Changing app.baseUrl or the origin of backend.baseUrl affects CORS, cookie domains, and OAuth redirect URIs registered with your identity provider, so test the change in staging first. After each deployment, load the portal in a browser, open the developer tools network panel, and confirm that API calls go to the intended backend origin with no mixed content or CORS errors. Keep backend and identity provider credentials out of every frontend file, including any rendered config. Record in your runbook which mechanism each environment uses, so an operator knows whether a value change requires a rebuild or only a config update.
Security risks to address when the frontend is exposed separately
Once the frontend is served from its own origin, the browser bundle, the edge proxy and the backend API each carry part of the security boundary. The cards below cover the decisions that connect them, from what NGINX or a CDN publishes to how every API call is authenticated.
Publish only static assets
Only the built static output and the entry HTML should be reachable from the public side. Every backend route, including the /api prefix and any plugin path, should be proxied to the backend or blocked at NGINX, not passed through to the frontend. Check with a request to each backend path from outside the network that it returns a proxy error or an authentication failure rather than content. Confirm the exact path list against your own plugin set.
Keep secrets out of the bundle
Everything the frontend reads from configuration is shipped to every browser that loads it, so any value with a secret in it is public. Only public URLs and identifiers belong in the frontend configuration. Search the built bundle for token prefixes, client secrets and internal hostnames before each release, and keep credentials in the backend configuration only.
Content security policy and security headers
Set a Content-Security-Policy and related headers at NGINX or the CDN, because the frontend no longer receives them from the backend. Allow only the script, style, image and connect sources the portal actually loads, including the backend origin for API calls. Verify headers on the HTML response and on the JavaScript and CSS files, since a policy that covers only the HTML is incomplete.
TLS termination and origin trust
Terminate TLS at the edge with certificates your team renews, and decide whether traffic between NGINX and the backend is also encrypted. Set Strict-Transport-Security only after you have confirmed HTTPS works on every hostname. Check that the backend origin in the frontend configuration uses the public HTTPS URL, and that cookies and redirects keep the secure flag through the proxy.
Authenticate every API call
Serving the frontend separately does not remove the backend's responsibility to authenticate requests. Each call from the browser must carry the session the backend expects, and cross-origin rules should allow only the frontend origin. Test each sign-in provider from the separated frontend, then confirm that an unauthenticated request to a catalog or scaffolder endpoint is refused.
Disable guest access in production
The authentication documentation advises against offering guest access in production, and the separated frontend should not reintroduce it through a proxy rule or a permissive configuration key. Remove any guest provider from the production configuration and confirm the sign-in page offers only your organization's providers. Map every provider to the same identity through sign-in resolvers, so that an anonymous session cannot be treated as a catalog user.
What the split changes during Backstage upgrades
Every Backstage upgrade touches the frontend build, so the NGINX or CDN setup must be retested with each release. Backstage upgrades use yarn backstage-cli versions:bump to move all @backstage packages together, and the Backstage Upgrade Helper shows the template changes between versions. Once the backend no longer serves the bundle, a version bump changes two things you own: the static bundle that NGINX or the CDN serves, and the app-config values baked into that bundle at build time. Treat the upgrade as a change to both halves. After each bump, rebuild the frontend, confirm that the served index.html references the new bundle, and check that routes such as deep links, sign-in redirects and the API base URL still resolve through your proxy rules. Review the Upgrade Helper output for any change to the frontend entry points or configuration keys, and record which of your NGINX or CDN settings it touches.
High and critical vulnerabilities are backported to recent releases where feasible, so the frontend rebuild cycle is driven by security as well as feature upgrades. The Backstage versioning policy says high and critical severity vulnerabilities will always be backported to releases from the last 6 months if feasible, and other security fixes may be backported. In practice, a security patch can arrive on a release line you are running, and because your frontend bundle is a separate artifact, you cannot wait for a backend redeploy to pick it up. Plan a rebuild and redeploy path for the frontend that can run outside your normal upgrade window, and confirm with your team which release line you track, how quickly you can publish a new static bundle, and how the CDN cache is purged. Also confirm that the Node.js version used for the build still matches a supported release on the Backstage versioning policy, since the policy lists the supported pair for each Backstage version.
Questions teams ask before serving the frontend separately
Can our own team split the Backstage frontend from the backend?
Yes, a team with working knowledge of Node.js, Backstage and NGINX can do the split, and most of the work is configuration and deployment change rather than new product code. The change touches three areas: removing the app-backend plugin from the backend, building the frontend as static files, and supplying the configuration the backend used to inject into the browser. Your team will own the plugin list, sign-in setup and the release process afterward. A partner adds the most value when plugins are custom, sign-in is complex, or the CDN and caching rules must follow your security policy. Ask for a written scope that lists which tasks the partner performs and which ones your team keeps.
What drives the effort of serving Backstage frontend separately?
Four factors shape the effort most: the number of plugins and how much custom frontend code they contain, the authentication setup, the CDN and caching layer, and how frontend configuration reaches the browser.
- Plugins: each frontend plugin must load correctly from the static bundle, and custom plugins that assume same-origin backend routes need review.
- Authentication: redirects, cookies and cross-origin requests between the frontend and backend hostnames must match your chosen provider. The Backstage authentication documentation covers the sign-in model.
- CDN: cache headers, invalidation on each release and handling of internal-only assets add design work.
- Configuration: values the backend once served must reach the bundle at build or run time. Confirm the approach for your Backstage version before you start.
How long does a separate Backstage frontend deployment take?
Duration depends on scope, not on a fixed figure. A single-environment setup with standard plugins and existing sign-in takes less time than a multi-environment setup with custom plugins, a CDN and a change to the identity provider. Time also goes to testing in non-production environments that match production hostnames, sign-in and access rules. Ask any provider for a timeline tied to a written scope with milestones for build, staging test, cutover and handover. Confirm whether the timeline covers the first upgrade after the split, because each Backstage release also needs a new frontend build.
What does our team need to provide for this work?
Your team typically supplies access and decisions that only your organization can give. Expect to provide:
- Access to the Backstage source repository and the CI system that builds images and bundles.
- The
app-configfiles for each environment, including the hostnames for the frontend and backend. - Non-production hostnames and TLS certificates for the frontend, backend and any CDN origin.
- Administrator access or a change owner at the identity provider to register new redirect URIs.
- Hosting for NGINX or the CDN, with the platform team's agreement on network paths.
- A named owner who signs off on plugin behavior, plus a small group of users to test the portal before cutover.
Does removing the app-backend plugin break Backstage plugins?
It can, but the risk concentrates in a few places. Plugins that call only their own backend APIs usually keep working once routing and cross-origin settings are correct. Plugins that assume the frontend and backend share an origin, or that depend on the backend serving assets, need explicit checks. Test every installed plugin in staging, including sign-in, catalog views, software template runs, and any plugin that has its own backend part. Check each plugin's documentation for the version you run, and record the results so you can repeat the tests at the next upgrade.
Can we serve the Backstage frontend from a CDN instead of NGINX?
Yes. The built frontend is a set of static files, so any static host or CDN that supports single-page routing, cache control and HTTPS can serve it. NGINX is a common choice for self-hosted setups. A CDN improves delivery to distributed users but adds work: each release must invalidate cached assets, configuration that differs by environment must never be cached across environments, and the bundle location must pass your data handling review. Confirm with your security team whether an external CDN is allowed for an internal developer portal before you choose it.
How does frontend separation affect Backstage upgrades?
Each Backstage release brings a new frontend bundle, so the static deployment must be rebuilt and redeployed together with the backend version. Keep both on the same release. Upgrades move all @backstage packages together with yarn backstage-cli versions:bump, and the Backstage Upgrade Helper shows template changes between versions. The main release line ships monthly and the next line ships weekly, so plan a recurring upgrade window. The versioning policy says high and critical vulnerabilities are backported to releases from the last 6 months where feasible. Follow the keeping Backstage updated guide for each step.
How DeployOpen scopes and delivers the frontend split
DeployOpen scopes the frontend split in writing before any change is made to your Backstage repository or servers. The agreed scope lists each deliverable: the frontend bundle build and its output directory, the NGINX or CDN configuration, the runtime configuration method, the identity redirect settings for each sign-in provider, the verification checks and the handover documentation. Because most engagements are fixed-scope, the number of backend and frontend hostnames, the number of sign-in providers and whether a CDN is already in place are the inputs that most change the quote. Ask for these to be stated explicitly in the written scope so that the quote and the delivered work match.
Delivery follows the scope in a fixed order. The build change produces a static bundle that the app-backend plugin no longer serves. The web server or CDN is then configured for cache rules, a single-page application fallback, security headers and the origin that the backend allows for cross-origin requests. Runtime configuration is supplied through the method agreed in scope, and each identity provider redirect URI is tested against the new frontend origin before cutover. Verification covers sign-in with every enabled provider, catalog and TechDocs loading, and a check that no secrets appear in the served bundle. DeployOpen runs all of this in your own cloud accounts, data centers or air-gapped network, and the handover includes the configuration and runbooks your team keeps. DeployOpen prepares these systems and evidence for compliance assessments, but it does not issue audit reports, attestations or certifications. Independent auditors and certification bodies do that work.
Ongoing support and upgrade retainers can cover changes to this layer after handover. Upgrades matter here because Backstage moves its frontend packages together with the rest of the @backstage set, so a version bump can change how the bundle is built or configured. Confirm with DeployOpen whether the retainer includes re-verifying sign-in redirects and the CDN rules after each upgrade your team applies, and whether it covers changes you make when your Backstage release line moves forward.

