Multi-Frontend
The Backend For Frontend pattern basically states that there should be a single backend for each frontend. While for some applications / architectures this makes a lot of sense, because there is a 1-to-1 mapping between the API surface and the browser based application, for some other architectures this may not be useful.
Especially in micro-service based architectures, where there are many backend APIs and multiple frontends using these APIs, having a dedicated backend service for each frontend introduces quite a lot of operational overhead.
To overcome this issue, a single BFF instance can support multiple frontends. Each frontend you configure can:
- Define its own OpenID Connect configuration
- Define its own Cookie settings
- Define its own API surface
- Be identified either via path based routing and/or origin selection.
Adding additional frontends to the BFF has very little impact on the performance on the BFF itself, but keep in mind that the traffic for all the frontends is proxied through the BFF.
Authentication Configuration
Section titled “Authentication Configuration”When you use multiple frontends, you can’t rely on manual authentication configuration. This is because each frontend requires its own scheme, and potentially its own OpenID Connect and Cookie configuration.
Instead, you should rely on automatic authentication configuration.
Below is an example on how to configure multiple frontends.
var bffBuilder = builder.Services .AddBff();
bffBuilder .ConfigureOpenIdConnect(options => { // These are the default values for all frontends. options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query";
options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false;
options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access");
options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; });
.AddFrontends(
// This frontend will use the default authentication options new BffFrontend(BffFrontendName.Parse("default-frontend")),
// This frontend uses most of the same authentication options, new BffFrontend(BffFrontendName.Parse("with-path")) .MapToPath("/with-path"), .WithOpenIdConnectOptions(opt => { // but overrides the clientid and client secret. opt.ClientId = "different-client-id"; opt.ClientSecret = "different secret"; }) .WithCookieOptions(opt => { // and overrides the cookie options to use 'lax' cookies. opt.Cookie.SameSite = SameSiteMode.Lax; });The order in which configuration is applied is
- programmatic default options (if any)
- default options from configuration (if any)
- frontend specific options (if any)
Each frontend can have custom OpenID Connect configuration and Cookie Configuration. This can both be configured programmatically as via Configuration.
Frontend Selection
Section titled “Frontend Selection”Each request to a frontend has to be uniquely defined by either its path, its origin or a combination of the two. If you specify neither, then it’s considered the default frontend.
Frontends are matched using the following algorithm:
- Selection by both origin and path: If there is a frontend that matches both the origin AND has the most specific match to a path, it’s selected.
- Selection by origin only: Then, if there is a frontend with only origins configured and it matches the path, it’s selected.
- Selection by path only: Then, if there is a frontend with a matching path specified, it’s selected.
- Default frontend: Then, if there is a default frontend configured, it’s selected.
In summary, the most specific match will be selected.
Implicit Frontend Disabled
Section titled “Implicit Frontend Disabled”When you don’t add any frontends, BFF creates an implicit default frontend. This allows BFF to function correctly in single frontend mode. As soon as you add a frontend, this implicit frontend is disabled.
If you want to use both explicitly matching frontends (on host headers or paths) and a default (fallback) frontend, you should explicitly add this default frontend.
Adding A Frontend During Startup
Section titled “Adding A Frontend During Startup”The simplest way to add frontends is during startup.
services .AddBff() .WithFrontends(new BffFrontend(BffFrontendName.Parse("frontend1")));You can call WithFrontends with multiple frontends in one go, or call it multiple times.
Adding / Updating A Frontend Dynamically At Runtime
Section titled “Adding / Updating A Frontend Dynamically At Runtime”If you want to manipulate the frontends at runtime, you can do so via the IFrontendCollection interface.
var frontends = app.Services.GetRequiredService<IFrontendCollection>();
frontends.AddOrUpdate(new BffFrontend(name));
frontends.Remove(name);Defining The API Surface
Section titled “Defining The API Surface”A frontend can define its own API surface, by specifying remote APIs.
var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithRemoteApis( // map the local path /path to the remote api new RemoteApi("/some_path", new Uri("https://remote-api")))
// You can also configure various options, such as the type of token, // retrieval parameters, etc.. new RemoteApi("/with_options", new Uri("https://remote-api"))) .WithAccessToken(RequiredTokenType.UserOrClient), .WithAccessTokenRetriever<ImpersonationAccessTokenRetriever>(), .WithUserAccessTokenParameters(new BffUserAccessTokenParameters { Resource = Resource.Parse("urn:isolated-api") }));See the topic on Token Management for more information about the various token management options.
Handling SPA Static Assets
Section titled “Handling SPA Static Assets”BFF can be configured to handle the static file assets that are typically used when developing Single-Page Application (SPA)-based app.
Proxying Only index.html
Section titled “Proxying Only index.html”When deploying a multi-frontend BFF, it makes most sense to have the frontend’s configured with an index.html file that is retrieved from a Content Delivery Network (CDN).
This can be done in various ways. For example, if you use Vite, you can publish static assets with a base URL configured. This will make sure that any static asset, (such as images, scripts, etc) are retrieved directly from the CDN for best performance.
var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithCdnIndexHtml(new Uri("https://my_cdn/some_app/index.html"))When you do this, the BFF automatically wires up a catch-all route that serves the index.html for that specific frontend.
See Serve the index page from the BFF host for more information.
Proxying All Static Assets
Section titled “Proxying All Static Assets”When developing a Single-Page Application (SPA), it’s very common to use a development webserver such as Vite. While Vite can publish static assets with a base URL, this doesn’t work well during development.
The best development experience can be achieved by configuring the BFF to proxy all static assets from the development server:
var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithProxiedStaticAssets(new Uri("https://localhost:3000")); // https://localhost:3000 would be the URL of your development web server.While this can also be done in production, it will proxy all static assets through BFF. This will increase the bandwidth consumed by the BFF and reduce the overall performance of your application.
Proxying Assets Based On Environment
Section titled “Proxying Assets Based On Environment”If you’re using a local development server during development and a CDN in production, you can configure asset proxying as follows:
// In this example, the environment name from the application builder is used to determine// if we're running in production or not.var runningInProduction = () => builder.Environment.EnvironmentName == Environments.Production;
// Then, when configuring the frontend, you can switch when the static assets will be proxied.new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithBffStaticAssets(new Uri("https://localhost:5010/static"), useCdnWhen: runningInProduction);