Several apps, one backend
Two Sinequa applications on the same server — two configurations, one session, and why isolating them is the mistake.
A Sinequa server hosts as many web applications as an administrator declares, each with its own query web services, facets, tabs and sort options. A portal offering an HR search and a technical search is one server and two apps.
Looking for two servers instead?
Two backends is the other case, and its rule is the opposite of this one — see Multiple backends. Here: one server, one session. There: two servers, two sessions kept apart.
The app travels with every request
app is read from the client's configuration on each request: it goes into the body of every mutating call
and into the query of api/v1/app. So two clients pointed at the same server with different app values see two
different configurations, without either knowing about the other.
The session must be shared
Same server means one origin, one cookie, one CSRF token. Give these clients the same storage prefix — or omit it, which is the default — and they share the session: the user signs in once.
Isolating them with distinct prefixes forces two sign-ins against the same server, and in credentials mode
that means two password forms. This is exactly the inverse of the multiple-backends rule.
Two applications, one sign-in
import { createAtomicClient } from "@sinequa/atomic";
import type { AtomicClient } from "@sinequa/atomic";
const backendUrl = "https://<your-sinequa-server>";
/**
* Same server, same session: the prefix is deliberately left at its default so
* both clients read the one stored token.
*/
export const hr: AtomicClient = createAtomicClient({ app: "<your-hr-app>", backendUrl });
export const tech: AtomicClient = createAtomicClient({ app: "<your-tech-app>", backendUrl });
/**
* Sign in once, then resolve each configuration. Order matters: two concurrent
* `login()` calls against one server would race for the same session.
*/
export const ready = (async () => {
await hr.initializeConfig();
await hr.auth.login();
// The session is already established; this only reads the other app's config.
await tech.initializeConfig();
})();
/** Each client searches its own application's query web services. */
export async function searchBoth(text: string) {
await ready;
return Promise.all([
hr.api.query.search({ name: "<your-hr-query>", text, pageSize: 5 }),
tech.api.query.search({ name: "<your-tech-query>", text, pageSize: 5 }),
]);
}import { InjectionToken, inject, provideAppInitializer } from "@angular/core";
import { type AtomicClient, createAtomicClient } from "@sinequa/atomic";
const backendUrl = "https://<your-sinequa-server>";
export type Apps = { hr: AtomicClient; tech: AtomicClient };
/**
* Same server, same session: the prefix is deliberately left at its default so
* both clients read the one stored token.
*/
export const APPS = new InjectionToken<Apps>("Sinequa apps", {
factory: () => ({
hr: createAtomicClient({ app: "<your-hr-app>", backendUrl }),
tech: createAtomicClient({ app: "<your-tech-app>", backendUrl }),
}),
});
export function provideApps() {
return provideAppInitializer(async () => {
const { hr, tech } = inject(APPS);
// Sign in once. Two concurrent login() calls against one server would race
// for the same session.
await hr.initializeConfig();
await hr.auth.login();
// The session is already established; this only reads the other app's config.
await tech.initializeConfig();
});
}import type { ApplicationConfig } from "@angular/core";
import { provideApps } from "./apps";
export const appConfig: ApplicationConfig = {
providers: [provideApps()],
};import { type AtomicClient, createAtomicClient } from "@sinequa/atomic";
const backendUrl = "https://<your-sinequa-server>";
/**
* Same server, same session: the prefix is deliberately left at its default so
* both clients read the one stored token.
*/
export const hr: AtomicClient = createAtomicClient({ app: "<your-hr-app>", backendUrl });
export const tech: AtomicClient = createAtomicClient({ app: "<your-tech-app>", backendUrl });
/**
* Sign in once, then resolve each configuration. Order matters: two concurrent
* `login()` calls against one server would race for the same session.
*/
export const ready = (async () => {
await hr.initializeConfig();
await hr.auth.login();
// The session is already established; this only reads the other app's config.
await tech.initializeConfig();
})();
export async function searchBoth(text: string) {
await ready;
return Promise.all([
hr.api.query.search({ name: "<your-hr-query>", text, pageSize: 5 }),
tech.api.query.search({ name: "<your-tech-query>", text, pageSize: 5 }),
]);
}Or one client, reconfigured
Two clients are for showing two applications together. When the user switches between them, one client is
enough — configure({ app }) changes it for every subsequent request:
import { createAtomicClient } from "@sinequa/atomic";
import type { CCApp } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-hr-app>",
backendUrl: "https://<your-sinequa-server>",
});
await client.initializeConfig();
await client.auth.login();
let configuration: CCApp | undefined = await client.api.app.get();
/**
* Switching takes effect on the next request. The configuration you were holding
* belongs to the previous application, so it has to be re-read — its facets, tabs
* and query names are all different.
*/
export async function switchTo(app: string) {
client.configure({ app });
configuration = await client.api.app.get();
return configuration;
}
export function currentQueryName() {
return configuration?.defaultQueryName;
}import { Injectable, inject, resource, signal } from "@angular/core";
import { SINEQUA } from "./sinequa";
@Injectable({ providedIn: "root" })
export class AppSwitcherService {
private readonly client = inject(SINEQUA);
readonly current = signal("<your-hr-app>");
/**
* Keyed on the selected application, so switching re-reads the configuration
* rather than keeping the previous one — its facets and query names differ.
*/
readonly configuration = resource({
params: () => ({ app: this.current() }),
loader: ({ params }) => {
this.client.configure({ app: params.app });
return this.client.api.app.get();
},
});
switchTo(app: string) {
this.current.set(app);
}
}import type { CCApp } from "@sinequa/atomic";
import { useEffect, useState } from "react";
import { ready, sinequa } from "./sinequa";
/**
* Switching takes effect on the next request, and the configuration has to be
* re-read: facets, tabs and query names all differ between applications.
*/
export function useAppSwitcher(initial: string) {
const [app, setApp] = useState(initial);
const [configuration, setConfiguration] = useState<CCApp>();
useEffect(() => {
let active = true;
ready
.then(() => {
sinequa.configure({ app });
return sinequa.api.app.get();
})
.then((next) => active && setConfiguration(next));
return () => {
active = false;
};
}, [app]);
return { app, switchTo: setApp, configuration };
}