Back to News & Insights
Artificial Intelligence August 25, 2026 · 6 min read

Chat history is a second read path into your RAG data — gate the replay like the search

My copilot persists the source cards it cites — which documents backed each answer, scores, names,...

Chat history is a second read path into your RAG data — gate the replay like the search

My copilot persists the source cards it cites — which documents backed each answer, scores, names, the works. That's table stakes for a trustworthy RAG product: an answer without its evidence is just vibes.

Here's the question that changed how I shipped it: six months from now, a user opens that old conversation and the cards render again. Who authorized them the second time?

The comfortable answer is "nobody has to — it's the user's own history, they already saw it." I shipped the uncomfortable answer instead, and I want to defend it:

Persistence is not permission. What a turn was allowed to show at write time proves nothing about what it may show at read time.

The short version, if you're skimming The moment you persist retrieval results — citations, source cards, snippets — your history endpoint becomes a second read path into the same data your search guards so carefully. Entitlements drift between write and read. Re-check authorization at read time, in the service, not just at the gateway. Degrade gracefully: when the finer-grained entitlement is off, withhold the document-derived cards but keep the conversation text. Authorization outcomes aren't all-or-nothing. Fail closed, and make denial look like absence: my history reads answer the same 404 for "no entitlement" as for "session doesn't exist".

Context in one sentence: I spent two weeks giving a streaming-first document-search copilot durable session history — persist every turn, restore the whole conversation after a refresh (the cursor-paging and UI-hydration half of that story is a post of its own ).

Part of that work was persisting the sources each turn emitted, pinned onto the audit event that produced them, so the transcript endpoint could replay them verbatim. And that's exactly where the trap is:

The live search path is guarded like a fortress — entitlement toggles, a fail-closed per-document view gate, identity from the JWT only. The new history endpoint reads document-derived data out of plain database rows, and the database doesn't know about any of that. Ship it naively and you've built an unguarded side door into the exact data you spent months gating.

Nothing about this is exotic. Every RAG product that persists retrievals has this door. The only question is whether anyone put a lock on it.

In this product, tenant admins control AI entitlements with toggles: the copilot itself, and separately the document-search capability that produces the source cards. Between the day a turn was written and the day it's replayed, weeks pass. Toggles flip. Contracts change. The rows don't care.

So I made the replay re-derive its answer from today's entitlements, not from the fact that the rows exist:

| What drifted since the turn was written | What the replay shows now | |---|---| | Nothing — everything still on | Full transcript + source cards | | Document-search toggle switched off | Transcript text replays; source cards withheld | | Copilot entitlement switched off | History answers 404 — no sessions, no turns |

Two different denial shapes, on purpose. That's the design decision the rest of this post unpacks.

The coarse gate sits at the top of every history read. It loads the tenant's entitlements and refuses before touching a single session row (identifiers lightly renamed for the post):

Three details doing quiet work here: "Never trusts the gateway." There is a gateway in front of this service doing its own gating. The service re-checks anyway, because the day someone reroutes traffic or adds a new caller, an assumption held in another codebase is not a control. 404, not 403. The turns endpoint already answers a uniform not-found for malformed, unknown, and foreign session ids, so it never confirms what exists. Entitlement denial joins the same posture — a denied caller learns nothing, not even "there's something here you can't have". Denial costs zero reads. The test pins this: when the toggle is off, the session and turn queries are never even awaited.

The finer case is more interesting: the copilot is on, but the document-search toggle is off. A blanket 404 would be wrong — the user's history isn't all about documents, and their conversations are still theirs. But the source cards are document-derived data: names, versions, relevance scores. They exist because a document search ran under an entitlement that is no longer granted.

The transcript text still replays. The cards don't. The test states the contract better than I can:

This is the part I'd push hardest in a design review: graceful degradation is an authorization outcome, not an error state. Most authz discussions collapse to allow/deny, and then someone argues "deny breaks the history feature, so… allow?" — and the side door ships open. Having a middle answer (keep the conversation, withhold the derived artifacts) is what made the strict position shippable at all.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation