Multiple backends
Two Sinequa servers on one page, each with its own session — and the one setting that keeps them from overwriting each other.
Two Sinequa servers at once: a federated view, a migration running old and new side by side, a tenant switcher. Each backend needs its own client, and each client needs its own storage prefix.
Looking for two applications instead?
Two apps on the same server is the other case, and its rule is the opposite of this one — see Several apps, one backend. There: one server, one shared session. Here: two servers, two sessions kept apart.
const a = createAtomicClient({ app: "a", backendUrl: "https://a.example.com", storagePrefix: "a:" });
const b = createAtomicClient({ app: "b", backendUrl: "https://b.example.com", storagePrefix: "b:" });Without distinct prefixes they share one session
storagePrefix defaults to an empty string — the same value the default client uses. Two clients created
without one write their CSRF token to the same storage key and overwrite each other, so whichever signed in
last wins and the other starts failing with a token that is not its own.
What a prefix isolates
Per client, independently: the stored token, the "authenticated with no token" flag, the credentials marker, the
provider redirect guard, the event subscribers, and the deliberate sign-out window. So one client signing out
leaves the other alone, and a 401 recovered on one does not touch the other.
The prefix is deliberately not part of client.config: it is fixed at creation, and configure() cannot
change it. Reading it back is client.storagePrefix — useful to tell two clients apart in a log.
A federated search
import { createAtomicClient } from "@sinequa/atomic";
import type { Article, AtomicClient, Query } from "@sinequa/atomic";
/** One client per backend, each with its own storage namespace. */
const clients: { label: string; client: AtomicClient }[] = [
{
label: "Corporate",
client: createAtomicClient({
app: "<your-app>",
backendUrl: "https://<first-sinequa-server>",
storagePrefix: "corp:",
}),
},
{
label: "Research",
client: createAtomicClient({
app: "<your-other-app>",
backendUrl: "https://<second-sinequa-server>",
storagePrefix: "research:",
}),
},
];
/** Each sign-in is independent: one backend being down must not block the other. */
export async function start() {
await Promise.all(
clients.map(async ({ label, client }) => {
try {
await client.initializeConfig();
await client.auth.login();
} catch (error) {
console.error(`${label} is unavailable:`, error);
}
}),
);
}
export type FederatedHit = { source: string; record: Article };
/** Query both, keep what answers, and label every hit with where it came from. */
export async function search(query: Query): Promise<FederatedHit[]> {
const answers = await Promise.allSettled(
clients.map(({ client }) => client.api.query.search(query)),
);
return answers.flatMap((answer, index) => {
if (answer.status !== "fulfilled" || answer.value.$error) return [];
return answer.value.records.map((record) => ({
source: clients[index].label,
record,
}));
});
}import { InjectionToken, inject, provideAppInitializer } from "@angular/core";
import { type AtomicClient, createAtomicClient } from "@sinequa/atomic";
export type Backend = { label: string; client: AtomicClient };
/** One client per backend. The prefixes are what keep the sessions apart. */
export const BACKENDS = new InjectionToken<Backend[]>("Sinequa backends", {
factory: () => [
{
label: "Corporate",
client: createAtomicClient({
app: "<your-app>",
backendUrl: "https://<first-sinequa-server>",
storagePrefix: "corp:",
}),
},
{
label: "Research",
client: createAtomicClient({
app: "<your-other-app>",
backendUrl: "https://<second-sinequa-server>",
storagePrefix: "research:",
}),
},
],
});
export function provideBackends() {
return provideAppInitializer(async () => {
const backends = inject(BACKENDS);
// Independent sign-ins: one backend being down must not block the other.
await Promise.all(
backends.map(async ({ label, client }) => {
try {
await client.initializeConfig();
await client.auth.login();
} catch (error) {
console.error(`${label} is unavailable:`, error);
}
}),
);
});
}import { Injectable, inject } from "@angular/core";
import type { Article, Query } from "@sinequa/atomic";
import { BACKENDS } from "./backends";
export type FederatedHit = { source: string; record: Article };
@Injectable({ providedIn: "root" })
export class FederatedService {
private readonly backends = inject(BACKENDS);
/** Query all backends, keep what answers, label every hit with its origin. */
async search(query: Query): Promise<FederatedHit[]> {
const answers = await Promise.allSettled(
this.backends.map(({ client }) => client.api.query.search(query)),
);
return answers.flatMap((answer, index) => {
if (answer.status !== "fulfilled" || answer.value.$error) return [];
return answer.value.records.map((record) => ({
source: this.backends[index].label,
record,
}));
});
}
}import { type AtomicClient, createAtomicClient } from "@sinequa/atomic";
import type { Article, Query } from "@sinequa/atomic";
/** One client per backend, each with its own storage namespace. */
export const backends: { label: string; client: AtomicClient }[] = [
{
label: "Corporate",
client: createAtomicClient({
app: "<your-app>",
backendUrl: "https://<first-sinequa-server>",
storagePrefix: "corp:",
}),
},
{
label: "Research",
client: createAtomicClient({
app: "<your-other-app>",
backendUrl: "https://<second-sinequa-server>",
storagePrefix: "research:",
}),
},
];
/** Independent sign-ins: one backend being down must not block the other. */
export const ready = Promise.all(
backends.map(async ({ label, client }) => {
try {
await client.initializeConfig();
await client.auth.login();
} catch (error) {
console.error(`${label} is unavailable:`, error);
}
}),
);
export type FederatedHit = { source: string; record: Article };
export async function searchAll(query: Query): Promise<FederatedHit[]> {
await ready;
const answers = await Promise.allSettled(
backends.map(({ client }) => client.api.query.search(query)),
);
return answers.flatMap((answer, index) => {
if (answer.status !== "fulfilled" || answer.value.$error) return [];
return answer.value.records.map((record) => ({
source: backends[index].label,
record,
}));
});
}Sharing a session on purpose
An empty prefix is right when you want one session — a client created to get typed calls alongside existing free-function code, for instance. Both then read the same storage, which is the point:
// Runs on the same session as fetchQuery(), setGlobalConfig(), and the rest.
const client = createAtomicClient({ app: "<your-app>" });getDefaultClient() goes further and returns the default client — the very instance the free functions use,
whose configuration is globalConfig.