Skip to content

ADR: Client-Scoped Sites, Knowledge, and Routing

Status

Accepted; amended 2026-09-17 (IX-4525): roots select markets within a shared workspace. Amended 2026-10-23 (IX-5696): that market layer is removed; each market is its own workspace.

Date

2026-07-16

Terminology note

IX-4926 renamed this client-facing container to workspace. This ADR retains site where it records the accepted physical schema (public.sites, site_id) and the historical decision that produced it. In current backoffice copy and frontend selection code, each selectable workspace maps to one of those site rows; site otherwise means the website placement served through its roots.

Context

Rose currently uses a domain as several identities at once: configuration target, site identity, and LightRAG tenant key. This works when a client has one website, one site configuration, and one knowledge corpus. It becomes limiting when a client operates several markets that share most content but differ on facts such as pricing, legal terms, product availability, or positioning.

Separate hostnames can be registered as separate domains today, which gives them separate configurations and retrieval tenants. Canonical domain aliases take the opposite approach and intentionally share both configuration and knowledge. Neither model covers all required cases:

  • a client may want locale sites on both separate hostnames and paths such as example.com/fr/
  • locale sites should share common behavior without copying a complete configuration
  • shared documents and FAQs should remain available in every market
  • market-specific knowledge must not leak between locales
  • a visitor may navigate between locale routes during an active conversation

The current LightRAG integration isolates data by tenant, but does not apply locale filters throughout vector and graph traversal. A merged graph summary cannot itself provide a reliable market isolation boundary; scoped answers must use attributable source chunks. Locale-specific relationships between the same entities can also conflict with one another.

This ADR separates site behavior, site knowledge, and website placement. Two words carry precise and different meanings throughout:

  • A site is the client-scoped container: one configuration target, one knowledge corpus, one or more roots. It is what the backoffice selects and what everything else is scoped by. This is the entity the ADR introduces.
  • An agent is a surface that works on a site — the Website Agent, the Content Agent, and later others. Agents live inside a site and read its knowledge. Neither word means an internal LangGraph node or a response-agent prompt.

Only the Website Agent consumes retrieval today, so this ADR describes retrieval, routing, and knowledge for a site as that agent uses them. Because the corpus belongs to the site rather than to one agent, a future retrieval-consuming agent such as the Nurturing Agent reads the same corpus by virtue of being on the same site. It needs no new entity, no type column, and no reference between corpora.

Decision

1. Model the site as the client-scoped identity

A client has one workspace per market that needs its own facts or behavior; a client with one market has one workspace. A workspace defines: - its enabled skills and behavioral configuration - its own knowledge corpus, addressed by an opaque storage key

The site, not the domain, is the long-term identity. Its identifier is a client-scoped slug, so roots may move without changing configuration, knowledge, or history. bot_key is the current name for this identity: resolve_bot_key(site_name) already maps a requested domain to a canonical bot, which is one site per client today.

The market and the response language are separate concepts. The workspace a root selects carries the market's behavior and facts; the response language may still follow the visitor or channel language.

Site configuration uses composition: the client's existing client-level configuration is the base, and each site stores only its overrides. This keeps shared behavior in one place while allowing locale-specific prompts, skills, presentation, and policy.

2. Give each site its own knowledge

Knowledge belongs to the site, not to a domain. Each site owns one retrieval boundary — its documents, FAQs, vectors, and graph — addressed by an opaque storage key. There is no separate knowledge entity to create, bind, or keep in sync.

Everything a workspace's corpus returns enters answer context: chunks, graph facts and FAQs. Market-specific knowledge stays apart because each market's workspace has its own corpus; content shared by several markets is indexed in each of them. Existing tenantId isolation stays mandatory.

3. Route website locations to sites

A website route maps an exact hostname plus a normalized path prefix — a root URL — to one site.

A site that has roots has exactly one canonical root. That root is the site's user-visible label; its roots define the ingest scope for its corpus. A site may also have any number of alias roots that resolve to the same site with no separate configuration:

example.com/*        workspace example.com (canonical)
example.com/fr/*     workspace example-com-fr
example.com/en-gb/*  workspace example-com-en-gb

Site identity remains the site, not its root URL. Roots may be added, renamed, or migrated without changing the site, its corpus, or its conversation and analytics history. A root URL is a placement, not a key. A site may also have no root at all, which is how a future non-website surface would be placed.

Routing follows one deterministic contract:

  1. Resolve the website boundary from authoritative request metadata as defined by the Website Boundaries ADR.
  2. Match the exact requested hostname within that boundary.
  3. Select the route with the longest matching path prefix.
  4. Use the hostname's required default route when no more-specific prefix matches.

Prefix matching is segment-boundary safe: a prefix matches when the requested path equals it or continues with /, so /uk never claims /ukraine. Hostnames are compared case-insensitively and paths case-sensitively, per RFC 3986, which makes /FR and /fr distinct roots.

Every configured hostname must have a default site route. Overlapping routes that would produce an ambiguous result are invalid configuration and must be rejected when written.

The backend resolves the route on every chat turn from trusted host and path data. A client-supplied site identifier is not authoritative. If navigation selects another site, the next turn uses the new site while preserving conversation history. Each turn records the resolved workspace and matched root so a transition is auditable.

4. Retrieve from exactly one corpus

A turn retrieves from exactly one workspace storage key, and its answer context is whatever that corpus returned. Conversation history is retained when navigation moves the visitor to another workspace; earlier turns are not rewritten.

A website route is enabled only after its site's corpus is ready. An unready corpus keeps the route disabled; it does not degrade to partial knowledge.

5. Preserve existing clients through a legacy mapping

Existing clients conceptually migrate to one legacy site owning its existing corpus. Current host and alias behavior remains unchanged until path roots and routes are explicitly configured:

  • existing standalone domains retain their independent behavior
  • existing canonical aliases continue to route to the same legacy site and knowledge, becoming additional roots at / on that site rather than a domain-to-domain relationship
  • existing LightRAG tenant data is reused rather than relabeled as a locale
  • a new path root is activated only after its workspace corpus is ready

Storage-key format and cache invalidation remain unspecified here. The physical schema, the data backfill, and the rollout sequence are specified below.

6. Physical schema

The identity model above is realised by two tables and one capability flag.

public.sites is the site: id UUID primary key, client_id, config_key TEXT UNIQUE, name, status (draft | live | disabled), and corpus_status (absent | indexing | ready | failed).

config_key is the stable configuration handle that config.client_configs keys on. It is assigned once and never derived from a root, which is exactly what lets a root move without changing configuration, knowledge, or history. A site whose canonical root is a bare host at / takes config_key = host, so every client that exists today keeps its current key and no configuration row moves; only a path-rooted site generates a slug. There is exactly one way to create each kind: registering a domain brings its site into being, and a path-rooted site is created explicitly.

public.site_roots is the placement: site_id, host, path_prefix (default /), is_canonical, and enabled (default false).

config.client_configs.domain deliberately keeps referencing public.domains. Repointing it at public.sites(config_key) looked free — every canonical domain becomes a site with an identical key — but production disproves the premise it rested on: some alias domains hold configuration rows of their own. Those rows are never read, because configuration resolution follows canonical_domain, but they exist, an alias never becomes a site, and deleting customer rows from a migration is not an option. Nothing in the identity phase needs the repoint: a site at a bare host is keyed by a registered domain and configures exactly as before, and only a path-rooted site cannot hold configuration — which cannot be routed to until the runtime phase regardless. The repoint belongs to the re-key phase, where those stale rows are resolved deliberately rather than as a side effect.

public.domains keeps its own job — hostname registration and the tenant boundary, per the Website Boundaries ADR — and a root's host must be a registered domain of the same client.

While canonical_domain still drives resolution, roots at / describe that graph rather than drive it. Reconciliation therefore belongs in the database, as one idempotent function behind both the initial backfill and a trigger on public.domains, so every writer — backoffice, backend, CLI, migration — keeps roots true. Roots authored by hand cannot drift from the behaviour they claim to describe.

Invariants belong in the database

Section 3 makes overlapping routes invalid configuration that is rejected when written. That is a constraint, not a warning. A backoffice surface may explain an invariant; it never stands in for one.

Invariant Mechanism
Host lowercased; path prefix normalized to a leading /, no trailing /, no //, no whitespace normalize trigger and check constraint
No two roots on one hostname and path prefix unique constraint
At most one canonical root per site partial unique index
A site with at least one root has exactly one canonical root deferrable constraint trigger
A hostname with any enabled root has an enabled root at / deferrable constraint trigger
A root's host is a registered domain of the same client trigger
Enabling a root requires the client capability, a live site, and a ready corpus trigger
A site leaving live or ready, or a client losing the capability, disables its roots in the same transaction triggers on both tables
Site creation is atomic security-definer function
A domain's site and its root at / always match canonical_domain idempotent reconciliation function, called by the backfill and by a trigger on public.domains

Two consequences of section 3 that are easy to get wrong:

  • Rootless sites are legal, so "exactly one canonical root" is conditional on having roots. A unique index can forbid duplicates but never absence, which is why the conditional half needs its own constraint trigger rather than an index.
  • The readiness gate must be bidirectional. A trigger that only refuses to enable a root leaves a site that later fails still serving. Losing readiness disables the affected roots in the same transaction.

The capability flag

public.clients.explicit_routing_enabled defaults to false. While it is false, no path-prefixed root can be enabled; flipping it back disables any that are and returns the client to legacy resolution, with no deploy. It is both the per-client opt-in and the rollback lever.

Roots at / are deliberately outside that gate. They describe hostname-level routing, which is what every client already has, and gating them would mean shipping a schema whose backfill contradicts production on day one. The gate exists to stop the new capability — routing by path — going live before the runtime phase can honour it, and a root at / is not that capability.

It is what allows the schema to ship before runtime resolution exists. Configuration routed by path while retrieval still resolves by hostname would answer one market from another market's corpus — a wrong answer that looks like a correct one. The flag makes that state unreachable rather than merely unlikely.

7. Phase mapping

Each delivery phase is one merge. Every merge must leave the trunk green and production behaviour unchanged, and a behaviour change is only permitted behind a flag that already exists in the database when the code that reads it lands.

Phase Delivers Gate before the next
Identity The schema above, the backfill, the invariants, and a staff surface for reading and authoring roots Existing clients' alias graphs read correctly as roots; widget configuration resolution is byte-identical
Ingestion Site-scoped crawl seeds and the corpus_status lifecycle A new site reaches ready from its own roots; no legacy corpus is touched
Runtime Host and path prefix to site per chat turn, storage key from the site, and path-aware widget configuration With the capability off, resolution is identical to today
Re-key Backoffice and analytics move from source hostname to site identity Expand, switch reads, then contract — three merges, not one
Pilot One client with the capability on The validation scenarios below, with evidence

Ingestion precedes runtime because nothing can be routed to a corpus that does not exist. Re-key depends only on identity, and is what lets a path-rooted site become selectable in the backoffice; until it lands, such a site is staff-visible only.

Model at a glance

Client: iBanFirst
├── Workspace: ibanfirst.com        root ibanfirst.com/
├── Workspace: ibanfirst-com-fr     root ibanfirst.com/fr
└── Workspace: ibanfirst-com-en-gb  root ibanfirst.com/en-gb
    each: its own configuration overrides, client skills, corpus and Website Agent

These roots are an onboarding example, not a change to iBanFirst's production data. A turn at /fr/pricing uses the FR workspace's configuration, skills and corpus; a turn at /en-gb/pricing uses the UK workspace's. Configuration still inherits the client base, so only the overrides differ.

Implementation Direction

Reuse the existing locale-aware skill filter

The current skill system already has a useful enforcement mechanism. Client skills are loaded from one canonical client library, and locale variants may use URL-prefix, URL-regex, or conversation-language conditions. The registry evaluates those conditions before skills are shown to the selector LLM, after the LLM returns a selection, and when always_on skills are injected. This prevents the selector from choosing a locale sibling that was not eligible.

The implementation retains that three-stage filter. A market's policy lives in its workspace's own client skills; URL conditions remain for page-specific behavior inside a workspace, and conversation_language stays the reply language, never the market.

Deliver incrementally

flowchart LR P1[Identity and legacy resolver] --> P2[Workspace-scoped ingestion] P2 --> P3[Site configuration and locale skills] P3 --> P4[Website routing, backoffice, and pilot]

Each phase has a compatibility exit gate. Existing clients continue through the legacy domain resolver and existing storage key until explicit routes are enabled. The pilot must prove skill isolation, retrieval isolation, route precedence, readiness gating, site transitions, and rollback before domain coupling is retired.

Relationship to Existing ADRs

Unified Configuration System

This ADR extends the unified configuration model with client-level base configuration and site-level overrides. It does not replace typed configuration definitions or their validation.

Website Boundaries and Host Routing

Website boundaries remain the source of truth for host trust, cookie scope, analytics identity, permissions, and origin validation. Site route selection occurs after that boundary is resolved and adds path-level placement. A site route is not a security boundary.

Subdomain Tenant Split via Explicit Domain Registration

Explicit domain registration remains valid existing behavior and a supported transitional mechanism. This ADR changes the long-term identity: domains route to sites instead of being the site and knowledge identity themselves.

Document Storage, DB Schema, and Conflict Model

The existing document lifecycle, provenance, snapshots, and conflict model remain applicable. Source-domain provenance and tenant isolation are unchanged. Each market's workspace indexes the content it answers from, shared pages included.

Consequences

  • Each market gets its own configuration overrides, skills, corpus and agent; content shared by several markets is indexed once per workspace.
  • A routed workspace answers from its whole corpus, FAQs and graph facts included.
  • Per-turn workspace and root snapshots preserve auditability through navigation and edits.
  • Existing domain-keyed config and persistence still migrate incrementally.

Validation Scenarios

  1. /fr and /en-gb resolve to their own workspaces, configurations and storage keys.
  2. A routed turn's answer context includes the workspace's FAQs and graph facts.
  3. Navigation between workspaces preserves session history.
  4. Readiness/environment failures remain strict; the widget bails cleanly.
  5. Flag-off and rootless clients retain legacy behavior, graph context and FAQs.

Alternatives Considered

Several markets in one workspace (IX-4525, removed in IX-5696)

Roots carried a market tag, several market roots shared one workspace, agent and corpus, and each turn filtered retrieved chunks to shared and current-market sources. Shipped in IX-4525 and removed in IX-5696 without ever being enabled: graph facts and FAQs have no reliable market provenance, so the filter dropped them for every workspace of a client as soon as one root had a market, and post-retrieval filtering cost recall. Removing it traded single indexing of shared content for whole-corpus answers.

Retrieve separate shared and market corpora

Deferred. It adds queries, merging and precedence rules. The approved design keeps one workspace corpus and one retrieval call.