@sinequa/atomic

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.

two backends, two sessions
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

client AstoragePrefix a: a:sinequa-credentialsa:sinequa-authenticated client BstoragePrefix b: b:sinequa-credentialsb:sinequa-authenticated own event bus own event bus

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.

Query two servers and merge the results
src/federated.ts
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,
    }));
  });
}

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:

one session, two APIs
// 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.

Pitfalls

What's next

On this page