# System Design: [Enterprise AI search name]

> Reference design for an enterprise search and answer service that
> retrieves from many workplace systems and answers with citations, while
> showing each user only what that user may read in the source system. It
> covers connectors, document-level and chunk-level permission filtering,
> permission sync, hybrid retrieval and reranking, citation grounding and
> answer verification, freshness, multi-tenant isolation and evaluation.
> Threats are mapped to the OWASP Top 10 for LLM Applications 2026, in
> particular LLM08:2026 Hidden Context Exposure and LLM09:2026 Vector and
> Embedding Weaknesses. It follows the 12-section skeleton of the System
> Design Document template and is pre-filled with a worked design for a
> multi-tenant SaaS to adapt. Replace every `[bracketed]` value and every
> row marked *Example*; all numbers are illustrative examples, not
> benchmarks.

<!--
Guidance: How to use this template
- The hard problem is permissions, not retrieval quality. A search that
  answers well but leaks one salary spreadsheet fails. Design the ACL path
  (7.2) before tuning ranking (7.3).
- Treat the language model context as visible to the user. Anything you
  put in it can come back out, so filter before generation, never after.
- The single-application RAG pipeline (chunking, embeddings, prompts) is
  covered by RAG System Design Template (`ai-data/rag-system-design`);
  this document adds the enterprise concerns around it.
- Model serving and vector index sizing are in LLM Inference Serving System
  Design (`system-design/design-llm-inference-serving`).
- Re-measure every example number on your own corpus before approval.
-->

## Document control

<!-- Guidance: Record the connectors and data classes in scope; reviewers from security and each data owner sign off. -->

| Field | Value |
|---|---|
| Document ID | [SDD-ERAG-001] |
| System / feature | [Enterprise search and answer service] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| Deployment model | [Multi-tenant SaaS / single-tenant / on-premises] |
| Connectors in scope | [Document storage, wiki, ticketing, chat, email, code hosting, CRM] |
| Data classes in scope | [Public, internal, confidential; excluded: regulated health or card data] |
| Related | [RAG system design], [Threat model], [Privacy impact assessment], [LLM evaluation plan] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead, search platform | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| Security and privacy | [@name] | [Decision] | YYYY-MM-DD |
| Data protection officer | [@name] | [Decision] | YYYY-MM-DD |
| Product owner | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Note what changed in the design, not the wording. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Added late permission check against the source for confidential connectors |
| 0.3 | YYYY-MM-DD | [@name] | *Example:* Dedicated shards for tenants above 50 million chunks |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page a busy reviewer can read alone. State the permission guarantee in one sentence; reviewers will hold you to it. -->

### 1.1 Problem statement

[*Example:* Employees search eight systems separately and cannot find the
current policy, the latest design review or who owns a service. A pilot
chatbot built on a single shared index answered well, but during testing it
quoted a restricted board document to an intern because the index ignored
source permissions. Customers of the SaaS product now ask for the same
capability across their own workplace systems, with a contractual promise
that no user ever sees content they cannot open in the source.]

### 1.2 Proposed solution

[*Example:* Connectors pull content and permissions from each source
system into a per-tenant index. Every chunk carries the principals allowed
to read it. At query time the service resolves the user's identity and
group memberships, retrieves only chunks whose principals intersect that
set, fuses lexical and vector results, reranks, and passes the top chunks
to a model that must cite them. A verifier checks that every sentence in
the answer is supported by a cited chunk before the answer is shown. For
confidential sources, the top documents are re-checked against the source
system's permission API at query time. Each tenant's data lives in its own
index partition with its own encryption key.]

**Permission guarantee:** [*Example:* An answer or result is built only
from chunks the requesting user could open in the source system at the
time of the last permission sync, and for confidential sources at the time
of the query.]

### 1.3 Decisions requested from reviewers

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Filter by permissions before or after retrieval? | Before (pre-filter in the index), plus late check for confidential sources | 7.2 |
| Q-02 | *Example:* Per-tenant index or shared index with tenant filter? | Pooled shards for small tenants with tenant key in every query; dedicated shards above a size threshold | 7.5 |
| Q-03 | *Example:* Show an answer when verification fails? | Show search results with an explanation instead of the answer | 7.4 |
| Q-04 | *Example:* Permission sync lag target? | p95 at or below 15 min; revocations on webhook within 2 min | 2.2 |
| [Q-nn] | [Question] | [Option] | [Section] |

### 1.4 45-minute interview path

<!-- Guidance: Order for an interview or a time-boxed review. -->

| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Users, sources, the permission guarantee, single or multi-tenant | 2.1 and 2.3 |
| 5 to 10 | Corpus size, query rate, permission change rate | 3.5 with consequences |
| 10 to 15 | Search and answer API; chunk and ACL data model with access patterns | 4.1 and 5.2 |
| 15 to 25 | Diagram: connectors, sync, index, query service, reranker, generator, verifier | Section 6 |
| 25 to 40 | One deep dive: permission filtering and sync (7.2) is the expected one; pick verification (7.4) for an ML panel or tenant isolation (7.5) for a SaaS panel | Options, choice and reason |
| 40 to 45 | Failure modes, 10x row, how to prove no leakage (9.2, 12.1) | 9.1 and 10 |

## 2. Requirements

<!-- Guidance: Core versus below the line. Put the permission guarantee in the requirements with a test, not only in the overview. -->

### 2.1 Core functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* Connectors for [document storage, wiki, ticketing, chat, email, CRM] with content, metadata and permissions | Must |
| FR-02 | *Example:* Search results and generated answers filtered by the user's source permissions | Must |
| FR-03 | *Example:* Chunk-level filtering where the source restricts parts of a document (page restrictions, protected ranges, message recipients) | Must |
| FR-04 | *Example:* Answers cite the source chunks with links that open the original item | Must |
| FR-05 | *Example:* Answers withheld or downgraded to results when verification fails | Must |
| FR-06 | *Example:* Incremental sync of content and permissions, with deletions honoured | Must |
| FR-07 | *Example:* Tenant administrators choose connectors, exclude sites and set retention | Must |
| FR-08 | *Example:* Feedback (helpful, wrong, should not see this) on each answer | Should |
| FR-09 | *Example:* Filters by source, author, date and content type | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Permission correctness | *Example:* Zero results from documents the user cannot read, on the canary suite in 9.2 | Daily automated leakage test |
| NFR-02 | Permission freshness | *Example:* p95 sync lag at or below 15 min; revocation by webhook at or below 2 min | Sync lag metric per connector |
| NFR-03 | Content freshness | *Example:* p95 at or below 30 min from source edit to searchable | Canary edits per connector |
| NFR-04 | Latency | *Example:* Search results p95 at or below 800 ms; first answer token p95 at or below 2.5 s | Gateway timing |
| NFR-05 | Answer quality | *Example:* Citation precision at or above 0.9 and answer faithfulness at or above 0.9 on the evaluation set | Offline evaluation per release |
| NFR-06 | Tenant isolation | *Example:* No query can read another tenant's partition; separate encryption key per tenant | Penetration test, key audit |
| NFR-07 | Availability | *Example:* 99.9% monthly for search, 99.5% for answers | SLO |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |

### 2.3 Below the line (out of scope)

- [*Example:* actions in source systems (creating tickets, sending email); see Customer Support AI Agent System Design (`system-design/design-customer-support-agent`) for an agent with tools]
- [*Example:* model training on tenant data]
- [*Example:* field-level redaction inside a chunk (for example, masking a salary column); chunks are visible or not]
- [*Example:* legal hold and e-discovery exports]
- [Item deferred, with the release that will take it]

## 3. Capacity estimation

<!-- Guidance: Every number ends in a consequence. The permission change rate is the number most designs forget. -->

### 3.1 Assumptions

All values are *Example* planning inputs.

| Input | Value | Source |
|---|---|---|
| Tenants | 400 | Sales forecast |
| Users across tenants | 1.5 million | Contracts |
| Daily active share | 30% | Pilot |
| Queries per active user per day | 6 | Pilot |
| Busy window | 10 h | Main time zones |
| Peak to average | 3 | Pilot |
| Documents across tenants | 600 million | Connector inventory |
| Chunks per document | 8 | Chunking policy |
| Embedding | 768 dimensions, int8 after quantization | Model choice |
| Documents whose permissions change per day | 2% | Connector audit logs |
| Candidates reranked per query | 50 | Retrieval design |
| Prompt / output tokens per answer | 6 000 / 300 | Ten chunks of about 500 tokens plus instructions |

### 3.2 Corpus and index

Chunks: 600 million x 8 = 4.8 billion. Vectors at 768 B each (int8):
4.8 billion x 768 B, about 3.7 TB. HNSW links at 64 per vector and 4 B each
add 4.8 billion x 256 B, about 1.2 TB. Lexical index and stored text add
roughly the size of the text again.

**Consequence:** about 5 TB of vector data plus text cannot sit on one
node. Shard by tenant: the largest tenants get dedicated shards, the long
tail shares pooled shards keyed by tenant. Filtered vector search must
work inside a shard, so pick an engine whose filters apply during graph
traversal, not after.

### 3.3 Query load

Daily queries: 1.5 million x 0.3 x 6 = 2.7 million. Over 36 000 s that is
75 queries/s on average and about 225 queries/s at peak. Reranking: 225 x
50 = 11 250 query and passage pairs per second at peak.

**Consequence:** a cross-encoder reranker over 11 250 pairs/s needs its own
GPU pool; cap candidates at 50 and skip reranking for navigational queries
(exact title or person look-ups) detected by the query classifier.

### 3.4 Generation tokens and cost

Input tokens per day: 2.7 million x 6 000 = 16.2 billion; output: 2.7
million x 300 = 810 million. At *Example* prices of USD 1 per million input
and USD 4 per million output: 16 200 + 3 240 = USD 19 440 per day.

**Consequence:** generation is the dominant cost. Answer only when the
query is a question (classifier), show results without generation for
navigational queries, and cache answers per (tenant, normalised query,
permission set hash, index version) for a short time, invalidating any
cached answer that cites a document with a revocation event.

### 3.5 Permission change rate

Documents with permission changes: 600 million x 0.02 = 12 million per day,
about 139/s on average. If principals are copied onto each chunk, that is
12 million x 8 = 96 million chunk updates per day, about 1 100/s.

**Consequence:** permission updates must be partial updates of a small
filterable field, not re-indexing of text and vectors. Group membership
changes are handled at query time by expanding the user's groups, so a user
joining a group never touches the index.

### 3.6 Summary

| Quantity | Estimate | Design consequence |
|---|---|---|
| Chunks | 4.8 billion, about 5 TB of vectors and links | Tenant-keyed shards, filtered ANN search |
| Peak queries | About 225/s | Stateless query service, regional replicas |
| Rerank pairs | About 11 250/s at peak | Dedicated reranker pool, skip for navigational queries |
| Generation | About 16 billion input tokens/day, about USD 19 000/day (*Example*) | Classify before generating; short-lived answer cache keyed by permission set |
| Permission updates | About 1 100 chunk updates/s if denormalised | Partial updates of the ACL field; group expansion at query time |

## 4. API design

<!-- Guidance: The API carries the caller's identity, never a list of groups supplied by the client. -->

### 4.1 Endpoints

| Method and path | Purpose |
|---|---|
| `POST /v1/search` | Ranked results with snippets, filtered by permissions |
| `POST /v1/answer` | Generated answer with citations, streamed; falls back to results |
| `GET /v1/documents/{id}/open` | Redirect to the item in the source system after a permission check |
| `POST /v1/feedback` | Helpful, wrong, or should-not-see report on a result or answer |
| `POST /v1/admin/connectors` | Tenant admin configures a connector and its scope |
| `GET /v1/admin/connectors/{id}/status` | Sync state, lag and errors |
| `POST /v1/admin/purge` | Remove a source or an item and all derived chunks |

### 4.2 Request and response example

Identity comes from the validated token; the service resolves groups
itself. Every citation carries the chunk ID, source link and the source's
last modified time so the user can judge freshness.

```http
POST /v1/answer
Authorization: Bearer [user token]
Content-Type: application/json

{"query": "What is the travel approval limit for directors?", "filters": {"sources": ["wiki", "policies"]}, "stream": true}
```

```json
{"answer": "Directors can approve travel up to [amount] per trip [1].", "citations": [{"id": 1, "chunk_id": "[chunk id]", "title": "Travel policy", "url": "[source link]", "modified": "YYYY-MM-DD"}], "verification": "supported", "index_version": "[version]"}
```

### 4.3 Errors

| Status | Problem type | Meaning |
|---|---|---|
| 403 | `identity-unresolved` | Token valid but the user is not mapped to source identities |
| 403 | `connector-forbidden` | User may not query this connector |
| 422 | `answer-unverified` | Answer failed verification; results are returned instead |
| 429 | `rate-limited` | Tenant or user rate limit reached |

## 5. Data model

<!-- Guidance: Show where permissions live and how they are joined at query time. The access-pattern table is the core of the review. -->

### 5.1 Entities

Documents and chunks are derived data that can be rebuilt from the source.
Identity mappings and ACLs are security data: they need the same review as
an authorisation system.

```mermaid
erDiagram
  TENANT ||--o{ CONNECTOR : configures
  CONNECTOR ||--o{ DOCUMENT : syncs
  DOCUMENT ||--o{ CHUNK : "split into"
  DOCUMENT ||--o{ DOC_ACL : "readable by"
  CHUNK ||--o{ CHUNK_ACL : "narrowed by"
  TENANT ||--o{ PRINCIPAL : has
  PRINCIPAL ||--o{ MEMBERSHIP : "member of"
  DOCUMENT {
    string tenant_id PK
    string doc_id PK
    string source_url
    datetime source_modified
    string sensitivity
  }
  CHUNK {
    string chunk_id PK
    string doc_id FK
    string allowed_principals
    string denied_principals
  }
  PRINCIPAL {
    string principal_id PK
    string kind
    string source_identity
  }
  MEMBERSHIP {
    string group_id PK
    string member_id PK
  }
```

### 5.2 Access patterns

| Query | Served by | Partition key and rationale |
|---|---|---|
| Vector and lexical search for a user | Shard for the tenant, filter `allowed_principals` intersects the user's principal set and `denied_principals` does not | `tenant_id`: every query is tenant-scoped, so a tenant never scans another tenant's shard |
| Expand a user's groups, including nested groups | Membership graph cached per tenant, flattened per user | `tenant_id` plus user: computed once per session, refreshed on change events |
| Apply a permission change to a document | Partial update of ACL fields on all chunks of the document | `tenant_id` plus `doc_id`: chunks of one document co-located for one batched update |
| Delete a document and its chunks | Delete by `doc_id` across lexical and vector indexes | Same co-location |
| Late permission check for top documents | Source system permission API, with a short cache | Per connector; only for sources marked confidential |
| Citation link resolution | Document table by `doc_id` | `tenant_id` |
| Connector sync cursor | Connector state table | `connector_id`: one sync worker owns one cursor |

### 5.3 Storage choices

| Data | Store | Notes |
|---|---|---|
| Chunks, vectors, lexical index, ACL fields | Search engine with filtered vector search, sharded by tenant | ACL fields filterable and not returned in results |
| Documents and connector state | Relational database per region | Sync cursors, source timestamps |
| Identity and membership graph | Graph or relational store, cached in memory | Rebuilt from directory sync |
| Raw content | Object storage, encrypted with the tenant key | Needed for re-chunking and re-embedding |
| Answers and feedback | Event log with tenant retention settings | Answers contain source content: same retention as the source |

## 6. High-level design

<!-- Guidance: One diagram, both paths: ingestion (content and permissions) and query. -->

```mermaid
flowchart LR
  subgraph Ingest
    src[(Source systems)] --> conn[Connectors]
    conn --> perm[Permission sync]
    conn --> parse[Parse and chunk]
    parse --> emb[Embedding workers]
    perm --> acl[(ACL fields and membership graph)]
    emb --> idx[(Tenant shards: lexical, vector, ACL)]
    acl --> idx
  end
  subgraph Query
    user([User]) --> gw[Gateway: identity, tenant, limits]
    gw --> qs[Query service]
    qs --> exp[Group expansion]
    exp --> acl
    qs --> idx
    qs --> rr[Reranker]
    rr --> late[Late permission check]
    late --> src
    late --> gen[Answer generator]
    gen --> ver[Citation verifier]
    ver --> user
  end
```

| Component | Responsibility | Scales by |
|---|---|---|
| Connectors | Fetch content, metadata, permissions; handle source rate limits | Workers per connector instance |
| Permission sync | Map source identities to principals; compute document and chunk ACLs; push partial updates | Change events per second |
| Parse, chunk, embed | Extract text, split on structure, embed with the pinned model | GPU workers |
| Tenant shards | Lexical and filtered vector search | Shards and replicas |
| Query service | Classify query, expand groups, hybrid retrieval, fusion | Horizontally |
| Reranker | Cross-encoder scoring of candidates | GPUs |
| Late check | Re-verify top documents with the source for confidential connectors | Horizontally, with cache |
| Generator and verifier | Answer with citations; check support per sentence | Model pools |

## 7. Detailed design

<!-- Guidance: Deep dives on the hard problems. Each one has options, a choice and a reason. -->

### 7.1 Query flow

The permission filter applies inside retrieval, before any ranking or
generation step sees a chunk; the late check narrows further and never
widens.

```mermaid
sequenceDiagram
  participant U as User
  participant Q as Query service
  participant I as Tenant shard
  participant R as Reranker
  participant S as Source system
  participant G as Generator
  participant V as Verifier
  U->>Q: question
  Q->>Q: classify, expand principals
  Q->>I: lexical and vector search with ACL filter
  I-->>Q: candidates (permitted only)
  Q->>R: rerank 50
  R-->>Q: top 10
  Q->>S: late check (confidential sources)
  S-->>Q: allowed subset
  Q->>G: question plus permitted chunks
  G-->>V: draft answer with citations
  V-->>U: verified answer or results
```

### 7.2 Deep dive 1: permission filtering and permission sync

| Option | How it works | Strengths | Weaknesses |
|---|---|---|---|
| A. Post-filter | Retrieve top k, then drop chunks the user cannot read | Simple index | Returns too few results; the model may see dropped chunks if the order of steps slips; leaks through counts and timings |
| B. Pre-filter in the index (early binding) | Principals stored on each chunk; query filter by the user's principals | Correct top k from permitted content only; fast | Index must be updated on every permission change; large principal sets |
| C. Query the source at query time (late binding) | Ask each source whether the user can read each candidate | Always current | Slow, source rate limits, many sources have no batch API |
| D. B plus C for sensitive sources | Pre-filter for all; re-check the top documents against the source for connectors marked confidential | Correct and fresh where it matters | Extra latency on confidential sources |

**Choice:** D. **Reason:** pre-filtering guarantees that nothing outside the
user's permissions enters the candidate set or the model context, and the
late check closes the sync-lag window for the few sources where a minute of
staleness is unacceptable.

Rules for the permission path:

| Rule | Detail |
|---|---|
| ACL-01 *(Example)* | Map every source identity (email, source user ID, group ID) to one tenant principal; unmapped identities get no access |
| ACL-02 *(Example)* | Store allow and deny principals on each chunk; deny wins |
| ACL-03 *(Example)* | Chunk ACL is the intersection of the document ACL and any section-level restriction (page restriction, protected range, message recipients) |
| ACL-04 *(Example)* | Public-within-tenant content uses a tenant-wide principal, never an empty ACL; an empty ACL means nobody |
| ACL-05 *(Example)* | Expand nested groups at query time from the cached membership graph; cap expansion depth and alert on cycles |
| ACL-06 *(Example)* | Revocation events (user removed, document unshared) are processed ahead of content updates in the sync queue |
| ACL-07 *(Example)* | On any error resolving permissions, return no results from that source (fail closed) |

Chunk-level permissions need care at chunking time: never let one chunk
span two sections with different restrictions. Split on permission
boundaries first, then on structure.

Large principal sets (users in thousands of groups) make filters slow. Use
the engine's terms filter on a keyword field, keep the field out of the
returned payload, and precompute a per-user principal list when it exceeds
a threshold.

### 7.3 Deep dive 2: hybrid retrieval and reranking

| Option | Strengths | Weaknesses |
|---|---|---|
| A. Lexical only (BM25) | Exact names, codes, acronyms | Misses paraphrases |
| B. Vector only | Paraphrases and intent | Weak on identifiers and rare terms |
| C. Hybrid with reciprocal rank fusion (RRF), then cross-encoder rerank | Combines both, robust without score calibration | Two retrievals plus a rerank stage |

**Choice:** C. Retrieve 50 from each retriever within the ACL filter, fuse
with RRF, rerank the top 50 with a cross-encoder, keep 10 for generation.
**Reason:** enterprise queries mix identifiers ("PRJ-2231 status") and
intent ("how do I expense a conference"); RRF needs no score
normalisation between retrievers.

RRF scores a document by summing, over each ranked list, one divided by a
constant plus its rank:

$$
\mathrm{RRF}(d) = \sum_{r \in R} \frac{1}{k + \mathrm{rank}_r(d)}, \qquad k = 60
$$

The constant 60 is the value used by Cormack, Clarke and Buettcher (SIGIR
2009); tune it on your evaluation set. Add freshness and authority signals
(for example, official policy space, recent modification) as features in
the reranker, not as hand-edited boosts.

### 7.4 Deep dive 3: citation grounding and answer verification

| Option | Strengths | Weaknesses |
|---|---|---|
| A. Instruct the model to cite | Cheap | Citations can point to chunks that do not support the sentence |
| B. Constrained citation format plus post-hoc check of every cited ID | Catches invented IDs | Does not catch unsupported claims with a real ID |
| C. B plus an entailment verifier per sentence | Catches unsupported sentences | Extra latency and cost |

**Choice:** C. **Reason:** an enterprise answer is acted on (policy,
numbers, owners); unsupported sentences are the main quality failure.

Verification steps:

1. The generator receives chunks with short IDs and must cite at least one
   ID per factual sentence.
2. The verifier rejects any citation ID not in the provided set.
3. For each sentence, a small verifier model judges whether the cited
   chunks support it (supported, partially supported, unsupported).
4. If any sentence is unsupported, the service removes it when the
   remaining answer still stands, otherwise returns results with "No
   verified answer found" and the top documents.
5. Verification outcome is stored with the answer for evaluation.

The generator's instructions tell it to answer only from the chunks, to say
when they disagree, and to prefer the most recently modified source while
naming both.

### 7.5 Deep dive 4: multi-tenant isolation

| Option | Strengths | Weaknesses |
|---|---|---|
| A. One shared index, tenant ID filter | Cheapest | A missing filter is a cross-tenant breach |
| B. Index per tenant | Strong isolation | Thousands of small indexes; operational overhead |
| C. Pooled shards for small tenants, dedicated shards above a threshold, tenant key enforced by the query service (not the caller) | Balance of cost and isolation | Two operating modes |

**Choice:** C, with a separate encryption key per tenant for raw content
and backups, and a query builder that adds the tenant filter itself and
rejects any query without one. Tenant routing follows the cell pattern in
Resilience and Load Shedding Design Template
(`system-design/resilience-load-shedding-design`).

### 7.6 How real systems do it

> **How real systems do it** *(paraphrased from Microsoft Learn, "Security
> filter pattern" for Azure AI Search, page dated August 2026)*. The
> documented pattern stores the user and group identifiers that may read a
> document in a filterable string collection field that is not returned in
> results, and applies a filter at query time that matches any of the
> caller's identifiers using a set-membership function rather than a long
> chain of equality tests, which the page notes is slower and error-prone
> with many identifiers. The page also warns that hiding the field from
> results is not itself a security control: authorisation comes from
> applying the filter on every query. The same documentation points to
> built-in document-level access control support for sources where it is
> available. Design lesson: pre-filter by principals on every query and
> never rely on field visibility for protection (7.2).

## 8. Scalability and performance

<!-- Guidance: Show how the design meets section 3 and where it breaks first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Corpus growth | Tenant-keyed shards, dedicated shards for large tenants | Largest single tenant in one shard group |
| Filtered vector search | Engine with in-traversal filtering; fall back to brute force for very selective filters | Recall drop when filters are very selective |
| Permission churn | Partial ACL updates, revocations prioritised | Search engine update throughput |
| Connector throughput | Respect source rate limits, back-off, per-tenant concurrency | Source API quotas |
| Generation cost | Classifier, answer cache keyed by permission set hash | Share of question-type queries |

Answer latency budget (target first token p95 2.5 s, *Example*):

| Step | Budget |
|---|---|
| Gateway, identity, group expansion | 60 ms |
| Lexical and vector retrieval in parallel | 150 ms |
| Fusion and rerank of 50 | 200 ms |
| Late permission check (confidential sources only) | 300 ms |
| Generator time to first token | 1 200 ms |
| Headroom | 590 ms |

Verification needs complete sentences: buffer the generator stream, release
each sentence to the user only after it passes, or show the answer after
verification when the interface does not stream. The first-token target
therefore applies to the first verified sentence, and the verifier's time
for that sentence comes out of the headroom.

## 9. Reliability, security and operations

<!-- Guidance: Failure modes with blast radius and RTO; threats mapped to OWASP; how you prove no leakage. -->

### 9.1 Failure-mode table

| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Permission sync | Stalled for one connector | Sync lag above 15 min | Revoked users may still see content from that source | Fail closed: exclude the connector from results when lag exceeds a hard limit | 30 min |
| Identity mapping | User not mapped after directory change | Unmapped identity count | User sees fewer results | Re-sync directory; alert tenant admin | 1 h |
| Tenant shard | Node loss | Shard health | Slower or partial results for tenants on that shard | Replicas; rebuild from object storage | 30 min |
| Reranker pool | Saturated | Queue depth | Lower ranking quality | Skip rerank, use fused order, mark degraded | 10 min |
| Generator | Outage | Error rate | No answers | Return results only | Per provider SLO |
| Verifier | Outage | Error rate | Unverified answers | Do not show answers; results only | 15 min |
| Connector | Source API change | Parse error rate | Stale content for a source | Version pinning of connector, contract tests | 1 day |

### 9.2 Security

Threat model per Threat Model Template (`security/threat-model`); privacy
per Privacy Threat Model Template (`security/privacy-threat-model`).

| ID | Threat | OWASP 2026 | Control |
|---|---|---|---|
| SEC-01 | User receives content they cannot read in the source | LLM02:2026 Sensitive Information Disclosure | Pre-filter (7.2), late check, fail closed, canary leakage tests |
| SEC-02 | Content the user may not see reaches the model context and is revealed by follow-up prompts | LLM08:2026 Hidden Context Exposure | Treat the whole context as visible to the user; only permitted chunks enter it; no other tenant or admin content in system prompts |
| SEC-03 | Cross-tenant retrieval, embedding inversion of stored vectors, poisoned documents that rank for many queries | LLM09:2026 Vector and Embedding Weaknesses | Tenant-scoped shards and query builder; vectors encrypted at rest and never returned by the API; anomaly detection on chunks that appear in unusually many answers |
| SEC-04 | Indexed document contains instructions that steer the answer | LLM01:2026 Prompt Injection | Chunks marked as data in the prompt; no tools available to the generator; verifier checks support |
| SEC-05 | Answer states wrong policy with confidence | LLM07:2026 Misinformation | Citation verification, "no verified answer" path, source dates shown |
| SEC-06 | Answer rendered as HTML with active content | LLM10:2026 Improper Output Handling | Sanitised Markdown rendering, link allowlist to source domains |
| [SEC-nn] | [Threat] | [OWASP ID] | [Control] |

Leakage testing: each tenant environment used for testing holds canary
documents restricted to a canary group, with unique marker strings. A daily
job queries as users outside that group and fails the build if any marker
appears in results, answers or citations. Run the same test after every
permission-related change.

### 9.3 Evaluation

Follow LLM Evaluation Plan Template (`ai-data/llm-evaluation-plan`).

| Metric | Definition | Target (*Example*) |
|---|---|---|
| Recall at 10 | Share of labelled relevant chunks in the top 10 after ACL filtering | At or above 0.85 |
| nDCG at 10 | Ranking quality on graded relevance labels | Tracked per release |
| Citation precision | Share of citations that support their sentence | At or above 0.9 |
| Citation recall | Share of factual sentences with a supporting citation | At or above 0.9 |
| Faithfulness | Share of answers with no unsupported sentence | At or above 0.9 |
| Leakage | Canary markers returned to unauthorised users | Zero |
| Freshness | Time from source edit to searchable | p95 at or below 30 min |

Labelled sets are built per tenant only with tenant consent; a synthetic
cross-industry set covers releases.

### 9.4 Observability

Trace each query with retrieval, rerank, late check, generation and
verification spans per LLM and Agent Observability Specification Template
(`ai-data/llm-observability-spec`). Log chunk IDs, not chunk text.

| Signal | Alert threshold (*Example*) |
|---|---|
| Permission sync lag per connector | p95 above 15 min |
| Late-check disagreements (pre-filter allowed, source denied) | Any sustained rate above 0.1% |
| Unverified answer rate | Above 10% |
| "Should not see this" feedback | Any report pages security on call |

## 10. Trade-offs and alternatives

<!-- Guidance: Options, costs and reasons, including what changes at 10x and 0.1x. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Permission enforcement | Post-filter; pre-filter; late binding; pre-filter plus late check | Pre-filter plus late check for confidential sources | Correct candidate set, fresh where it matters | Extra latency on confidential sources |
| Retrieval | Lexical; vector; hybrid | Hybrid with RRF and rerank | Mixed identifier and intent queries | More components |
| Verification | None; ID check; per-sentence entailment | Per-sentence entailment | Unsupported claims are the main failure | Latency and cost |
| Tenancy | Shared index; per tenant; pooled plus dedicated | Pooled plus dedicated | Cost and isolation balance | Two modes to operate |
| What changes at 10x (6 billion documents, 2 250 queries/s) | | Regional cells per tenant group; ACL stored as compact principal bitmaps; reranker distilled to a smaller model | Filters and rerank become the bottleneck | More engineering in the search engine |
| What changes at 0.1x (40 tenants, 60 million documents) | | Single cluster with per-tenant indexes; managed search service; no dedicated reranker pool, hosted rerank API | Fixed costs dominate | Less control over latency |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |

## 11. Delivery plan and risks

<!-- Guidance: Ship permissions first, then answers. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Two connectors, permission sync, search results only | Leakage canary suite passes for 14 days | YYYY-MM-DD |
| M2 | Hybrid retrieval and reranking | Recall at 10 target on evaluation set | YYYY-MM-DD |
| M3 | Answers with citations and verification | Citation precision and faithfulness targets | YYYY-MM-DD |
| M4 | Late check for confidential connectors; dedicated shards | Penetration test of tenant isolation | YYYY-MM-DD |
| M5 | Remaining connectors, admin controls | Per-connector contract tests in CI | YYYY-MM-DD |

### 11.2 Rollout and rollback

[*Example:* Enable per tenant and per connector. Answers are off by default
for a tenant until its leakage tests pass. Ranking and model changes roll
out behind an index version alias; rollback switches the alias.]

### 11.3 Risks

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* A source exposes permissions incompletely through its API | Medium | High | Connector certification; mark source confidential and use late check or exclude | [@name] |
| R-02 | *Example:* Users trust unverified answers | Medium | Medium | Verification, source dates, feedback | [@name] |
| R-03 | *Example:* Generation cost above forecast | Medium | Medium | Classifier, caching, model routing | [@name] |
| R-04 | *Example:* Oversharing in the source becomes visible through search | High | Medium | Tenant admin report of widely shared sensitive items before launch | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

R-04 deserves attention: search does not create oversharing, but it makes
existing oversharing easy to find. Offer tenants a pre-launch report.

## 12. Appendices

<!-- Guidance: Traceability, open questions, glossary and references. -->

### 12.1 Traceability

| Requirement | Design element | Verification |
|---|---|---|
| FR-02, NFR-01 | 7.2 pre-filter and late check | Canary leakage suite |
| FR-03 | 7.2 ACL-03, chunking on permission boundaries | Section-restriction test documents |
| FR-04, FR-05 | 7.4 verification | Citation precision and faithfulness |
| NFR-02 | 7.2 ACL-06, sync design | Sync lag metric |
| NFR-06 | 7.5 tenancy | Penetration test |
| [ID] | [Element] | [Test] |

### 12.2 Open questions

| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | *Example:* Do we index email at all, given per-message recipients? | [@name] | YYYY-MM-DD |
| OQ-02 | *Example:* Should tenants bring their own encryption keys? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |

### 12.3 Glossary

| Term | Meaning |
|---|---|
| ACL | Access control list: principals allowed or denied |
| Principal | User, group or tenant-wide identity used in ACLs |
| Early binding | Permissions stored in the index and filtered at query time |
| Late binding | Permissions checked against the source at query time |
| RRF | Reciprocal rank fusion of several ranked lists |
| Cross-encoder | Model that scores a query and passage together for reranking |
| Faithfulness | Answer claims supported by the provided sources |

### 12.4 References

| Reference | Use |
|---|---|
| OWASP Top 10 for LLM Applications 2026, OWASP GenAI Security Project, https://genai.owasp.org/ | Threat mapping in 9.2 |
| Cormack, Clarke and Buettcher, "Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods", SIGIR 2009 (doi:10.1145/1571941.1572114) | RRF in 7.3 |
| Lewis et al., "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks", NeurIPS 2020 (arXiv:2005.11401) | RAG pattern |
| Gao et al., "Enabling Large Language Models to Generate Text with Citations", EMNLP 2023 (arXiv:2305.14627) | Citation quality metrics in 9.3 |
| Microsoft Learn, "Security filter pattern", Azure AI Search, https://learn.microsoft.com/en-us/azure/search/search-security-trimming-for-azure-search | Box in 7.6 |
| RAG System Design Template (`ai-data/rag-system-design`) | Single-application pipeline details |
| Guardrails Specification Template (`ai-data/guardrails-spec`) | Input and output guardrails |
| LLM Inference Serving System Design (`system-design/design-llm-inference-serving`) | Model and vector index serving |

## Standard and licence note

Based on the OWASP Top 10 for LLM Applications 2026 by the OWASP GenAI
Security Project (https://genai.owasp.org/), licensed under Creative
Commons Attribution-ShareAlike 4.0
(https://creativecommons.org/licenses/by-sa/4.0/), and on the published
research and public documentation in 12.4. The OWASP IDs and short names
are used for the mapping in 9.2; all descriptions and controls are original
text. Because that mapping adapts the OWASP structure, the adapted
structure is shared under CC BY-SA 4.0 and any further adaptation must
carry the same licence. The box in 7.6 paraphrases public documentation
and copies no text. All numbers are illustrative examples, not benchmarks.
Check the current official versions of each source before use. Apart from
the adapted OWASP structure, the original text is under the template
licence. Template licence: MIT.
