Broadleaf Microservices

Tenant & Application Walkthrough

In this walkthrough, we’ll be examining how we handle resolving the tenant and application along with setting up the TenantContext for the downstream components. This walkthrough is mostly about the API calls since this occurs before we start rendering most components.

What is Tenancy?

Before we can load any content, we need to establish the TenantContext. This informs all API requests about the current Tenant and Application, which are used for narrowing the results of API requests to Broadleaf Microservices on the backend. "Tenancy" represents the ability to create hierarchies in our data and silo off portions of it for specific request contexts.

At the top, is the "Tenant". The Tenant represents a distinct, independent operator of the Broadleaf services. Tenants share the same infrastructure (e.g., services) but do not share data (e.g., catalogs, products, assets). Thus, a single instance of Broadleaf could be shared by multiple companies (i.e., Tenants) without allowing their data to be mixed together.

Beneath a Tenant reside its "Applications". A single Tenant can maintain multiple applications. An Application represents a storefront or commerce-facing frontend. All Applications have an identifier that defines part or all of a URL they can be resolved at. When resolution occurs, an Application can be identified by the domain, domain prefix, or a query parameter in the request’s URL.

For example:

  • Domain: www.my-application.com

  • Domain Prefix: my-application.my-company.com

  • Query Parameter: www.my-company.com?application=my-application

You can also map an Application to a specific frontend app like an instance of our Next.js Starter with a token instead of the URL. This is simpler and would apply if any single frontend app should only ever resolve to a single Application. To use a token, set TENANT_RESOLVER_APPLICATION_RESOLUTION=token and TENANT_RESOLVER_APPLICATION_TOKEN to the value set on the Application such as HEAT_CLINIC.

Catalogs, menus, and assets, then, become associated with Applications, thus tying those entities to a URL for commerce purposes.

Note
Tenants can also have associated URL components, but these are used for resolving the Admin app rather than the commerce one.

Tenants and Applications also contain important configuration details for frontends such as:

  • What locales their content supports for localization

  • What currencies they support

  • The default locale to localize content for

  • The default currency for the same

  • A reference to the logo to render, including its metadata such as the alt-text, title, and tags.

How to Resolve the Application

After identifying the Application, the endpoint then fetches the parent Tenant and returns both. The commerce app then sets up the TenantContext using this data. It will also cache the results of the request so that it doesn’t have to resolve it over and over again for subsequent requests as the users navigate the site.

Tip
Once the Application is resolved, the locales and currencies for the selectors will also be updated based on the default and allowed locales and currencies present.
Example 1. Response Payload
{
  "tenant": {
    "id": "string",
    "name": "BLC Tenant",
    "identifierType": "DOMAIN",
    "identifierValue": "my-company.com",
    "defaultLocale": "en-US",
    "allowedLocales": [
      "en",
      "es",
      "fr",
    ],
    "defaultCurrency": "USD",
    "allowedCurrencies": [
      "USD",
      "GBP",
      "EUR",
      "MXN"
    ]
  },
  "application": {
    "id": "2",
    "name": "Heat Clinic",
    "token": "HEAT_CLINIC",
    "identifierType": "DOMAIN_PREFIX",
    "identifierValue": "heatclinic",
    "customerContextId": "2",
    "deactivated": false,
    "defaultLocale": "en-US",
    "allowedLocales": [
      "en",
      "es",
      "fr",
    ],
    "defaultCurrency": "USD",
    "allowedCurrencies": [
      "USD",
      "GBP",
      "EUR",
      "MXN"
    ],
    "logoAsset": {
      "tenantId": "5DF1363059675161A85F576D",
      "type": "IMAGE",
      "provider": "BROADLEAF",
      "url": "/b2b-the-heat-clinic-logo.png",
      "contentUrl": "https://{my-domain}/api/asset/content/b2b-the-heat-clinic-logo.png?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%string%22%7D",
      "altText": "The Heat Clinic: Business",
      "title": "AAA Hot Sauces Logo",
      "tags": []
    },
    "attributes": {},
    "isolatedCatalogs": [
      {
        "id": "3",
        "implicit": "30",
        "name": "HEAT_CLINIC_MASTER_CATALOG",
        "mutabilityType": "CUSTOMIZABLE",
        "visibleAsAssigned": true,
        "excludeFromAdd": false,
        "catalogStatus": "ONLINE"
      }
    ],
    "marketplace": false
  }
}

URL-Based Resolution

Important
This assumes that TENANT_RESOLVER_APPLICATION_RESOLUTION=url.

When a user goes to a URL in their browser that maps to the app we’re running, the app first sends a request to the Tenant Resolver Endpoint This endpoint takes a single parameter—url—which should be the browser’s current location. The endpoint matches url against the Applications' identifiers.

Request to match against the domain

GET /api/tenant/resolver/application?url=https://www.my-application.com/

Request to match against the domain prefix

GET /api/tenant/resolver/application?url=https://my-application.my-company.com/

Request to match against a parameter

GET /api/tenant/resolver/application?url=https://www.my-company.com/?application=my-application

Example 2. With the Commerce SDK
import { ClientOptions } from '@broadleaf/commerce-core';
import { TenantClient } from '@broadleaf/commerce-tenant';

async function resolveApplication(options: ClientOptions) {
    const client = new TenantClient(options);
    return client.getApplicationByUrl(window.location.href);
}

Token-Based Resolution

Important
This assumes that TENANT_RESOLVER_APPLICATION_RESOLUTION=token.

In this case, when the user navigates to the app, we don’t need to look at the URL to determine which Application it belongs to. Instead we can look at TENANT_RESOLVER_APPLICATION_TOKEN.

Request to match against the token

GET /api/tenant/resolver/application?token=HEAT_CLINIC

Example 3. With the Commerce SDK
import { ClientOptions } from '@broadleaf/commerce-core';
import { TenantClient } from '@broadleaf/commerce-tenant';

async function resolveApplication(options: ClientOptions) {
    const client = new TenantClient(options);
    return client.getApplicationByToken(process.env.NEXT_PUBLIC_TENANT_RESOLVER_APPLICATION_TOKEN);
}

What if no Application is resolved?

It’s up to the implementor to decide how to handle this. It may make sense to define a "default" Application to always fallback to.