Bootstrap the application
Sign the user in and initialize the application's stores — and, optionally, its dynamic routes — before the first view renders.
bootstrapApp() is the one call most applications register as an provideAppInitializer: it resolves the
authentication mode, signs the user in, and initializes the application's stores in the right order, so no
component has to guard against a half-initialized application.
Minimal example
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient } from "@sinequa/atomic-angular";
export const appConfig: ApplicationConfig = {
providers: [
provideAtomicClient({ app: "my-app" }),
provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
],
};How it works
Resolving backendUrl and the authentication mode before signing in is what removes a long-standing
bootstrap race: every store or service that derives its API URL from the client's configuration is only
constructed after that resolution, so none of them can capture an unresolved backend URL. bootstrapApp also
wires the client's recoverFromUnauthorized hook to signIn(), so a 401 on any of the client's own requests
re-authenticates and replays the request once — the same recovery the HTTP interceptors provide for requests
the client never sees (see Cross-cutting interceptors).
Once signed in, ApplicationService reads what every view needs — the app configuration, the principal and
the user settings — in parallel: none depends on another's answer, only on the session, so they take one
round trip rather than three. It then applies them to AppStore, PrincipalStore and UserSettingsStore in
that order, resetting QueryParamsStore between the principal and the settings. A failed read is handled
exactly as before: the configuration and the principal stop the start-up, the settings fall back to their
defaults.
bootstrapApp keeps the client's uiLanguage on the interface language rather than a value fixed at
start-up: it follows TranslocoService.langChanges$ for as long as the application runs, which is also what
the request body interceptor and the syslang/translocoDate pipes read on their own side (see
Cross-cutting pipes).
Recipes
Read the start-up data in one request
Without it, a signed-in start-up costs three requests sent together — GET api/v1/app,
GET api/v1/principal, GET api/v1/user-settings. Behind a backend-for-frontend (BFF) that makes those calls
itself, server-side, and answers them all at once — mint-dev-server's BFF answers GET /bff/bootstrap —
provideStartupAggregation() replaces all three with a single request. Nothing else changes in the
application: the stores are filled exactly as before.
Add it only when the application is actually served behind such an endpoint. Against a Sinequa server reached
directly, it brings nothing and costs a round trip on every start-up: the request answers 404, a warning is
logged, and only then do the parallel reads start. Those parallel reads are the default — they need no
provider.
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient, provideStartupAggregation } from "@sinequa/atomic-angular";
export const appConfig: ApplicationConfig = {
providers: [
provideAtomicClient({ app: "my-app" }),
// Defaults to "bff/bootstrap" on the backend.
provideStartupAggregation(),
provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
],
};The endpoint answers with a StartupData: app, principal and userSettings, each one a StartupPart — { "ok": true, "value": … } when the BFF read it, { "ok": false, "status": … }
when it could not. Any other key — mint-dev-server's BFF also sends userProfile — is ignored: the profile is
read when the account settings panel opens, as without the provider. Here the settings call failed on the
server:
{
"app": { "ok": true, "value": { "name": "my-app", "queries": { "…": "…" } } },
"principal": { "ok": true, "value": { "userId": "jdoe", "name": "Jane Doe", "…": "…" } },
"userSettings": { "ok": false, "status": 503 }
}With that answer, AppStore and PrincipalStore take their value without a request, and GET api/v1/user-settings is sent from the browser: every part the BFF could not read — or that its answer lacks —
is fetched the usual way, all of them in parallel, and a failure of that read is handled as before. Only the
browser's own reads can fail the start-up: the status or error a part reports is never treated as a
failure.
If the request itself fails, or answers no data at all (an empty 204), the start-up does not stop: a warning
is logged and it falls back to the three parallel reads — one round trip later. A failed request is also a
failed request of the client: with provideAtomicNotifications(), a 400, 403, 500
or 503 shows an error toast, so a BFF that is down is visible to the user.
Use another endpoint path, or opt in per environment
The argument is the endpoint's path on the backend, resolved like every request of the client — relative to
its backendUrl. Since the provider is an ordinary EnvironmentProviders, an application that runs both with
and without a BFF can add it conditionally.
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient, provideStartupAggregation } from "@sinequa/atomic-angular";
import { environment } from "./environment";
export const appConfig: ApplicationConfig = {
providers: [
provideAtomicClient({ app: "my-app" }),
// GET <backendUrl>/gateway/startup — and nothing at all where there is no BFF.
...(environment.hasBff ? [provideStartupAggregation("gateway/startup")] : []),
provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
],
};export const environment = { hasBff: true };A failing endpoint never breaks the start-up, but it delays it: the condition is what spares the backends without BFF a failed request and an extra round trip on every start-up.
Read the start-up data from somewhere else
provideStartupAggregation() is a shortcut for one STARTUP_DATA_SOURCE — a function returning a
Promise<StartupData> that ApplicationService calls on every initialization: at start-up, and again after
the sign-in form or a password change. Provide the token yourself to read the data some other way: here, from
a JSON block the server inlined into index.html, which costs no request at all. The same rules apply: a part
marked { ok: false }, or missing, is read the usual way, and a source that throws makes the start-up fall back
to the parallel reads.
The inlined data describes the session the page was rendered for, so the source reads it once and removes it: after a sign-out and a sign-in — possibly as someone else — it throws, and the three parts are read again.
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient, STARTUP_DATA_SOURCE, StartupData } from "@sinequa/atomic-angular";
// index.html, rendered by the server:
// <script id="startup-data" type="application/json">{ "app": { "ok": true, "value": … }, … }</script>
async function readInlinedStartupData(): Promise<StartupData> {
const script = document.getElementById("startup-data");
const json = script?.textContent;
// Only valid for the first initialization: a later one may be another user's.
script?.remove();
// Throwing falls back to the three parallel reads.
if (!json) throw new Error("No inlined start-up data");
return JSON.parse(json) as StartupData;
}
export const appConfig: ApplicationConfig = {
providers: [
provideAtomicClient({ app: "my-app" }),
{ provide: STARTUP_DATA_SOURCE, useValue: readInlinedStartupData },
provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
],
};A source does not have to provide every part. One that only knows the application configuration — cached from
a previous visit, say — answers the others with { ok: false } (or leaves them out), and they are read the
usual way, in parallel:
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import type { CCApp } from "@sinequa/atomic";
import { bootstrapApp, provideAtomicClient, STARTUP_DATA_SOURCE, StartupData } from "@sinequa/atomic-angular";
async function cachedAppOnly(): Promise<StartupData> {
const cached = sessionStorage.getItem("my-app:config");
return {
app: cached ? { ok: true, value: JSON.parse(cached) as CCApp } : { ok: false },
// Not read here: both are read the usual way, together.
principal: { ok: false },
userSettings: { ok: false },
};
}
export const appConfig: ApplicationConfig = {
providers: [
provideAtomicClient({ app: "my-app" }),
{ provide: STARTUP_DATA_SOURCE, useValue: cachedAppOnly },
provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
],
};Options
Prop
Type
provideStartupAggregation(endpoint?) takes the endpoint's path on the backend, "bff/bootstrap" by default.
To read the start-up data some other way, provide STARTUP_DATA_SOURCE with a function returning a
Promise<StartupData>.
The promise bootstrapApp returns never rejects — it resolves true once fully initialized, false when the
user is not authenticated or an error occurred (already logged and, for a fatal error, already routed to
/error).