cd /news/artificial-intelligence/your-rag-cache-can-resurrect-a-delet… · home › topics › artificial-intelligence › article
[ARTICLE · art-159093] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Your RAG Cache Can Resurrect a Deleted Document

A developer published a TypeScript reference implementation, tiny-rag-revocation-gate, that demonstrates how retrieval caches in RAG pipelines can serve deleted or permission-revoked documents into a model's context. The gate caches only document IDs and revisions, then re-validates each candidate against an authoritative snapshot of tenant, deletion state, reader list and revision before any text enters the prompt, capping requests at 100 candidates and rejecting duplicate or conflicting entries. The writeup notes that index freshness mechanisms such as Pinecone's log sequence numbers do not invalidate a separate application-level cache.

by read6 min views3 publishedOct 27, 2026

You delete a document.

The search index catches up. The source URL returns 404. The admin screen says it is gone.

Then someone asks your assistant a question, and the assistant quotes it.

The model did not break into your database. Your application handed it an old copy.

That is the uncomfortable thing about adding a cache to a retrieval pipeline: you have created another place where yesterday's permission can survive today's decision.

Let's build a small TypeScript gate that makes that failure visible. No model, no vector database, no API key. Just cached references, current document state, and a test that proves deleted content stays out of the context we build.

Code: tiny-rag-revocation-gate

Imagine an internal handbook at revision 7. Alex can read it, so a retrieval cache stores a result for a question about travel expenses.

Later, the handbook is deleted. The cache entry still has another twenty minutes to live.

A cache hit tells you that a previous retrieval produced a result. It does not tell you that the document still exists, that its content is current, or that Alex can still read it.

Those are different questions with different owners.

This becomes especially easy to miss when your vector index and your application database update independently. Pinecone documents eventual consistency: writes can take time to appear in query results. Its serverless API exposes log sequence numbers to check whether a query reflects a particular write.

That helps you reason about the index. It does not invalidate a separate application cache.

Even a perfectly fresh index cannot help a request that skips the index entirely.

For this example, the cache stores only this:

export type Candidate = {
  documentId: string;
  revision: number;
};

No document text. No cached claim that a user is allowed to read it.

The current record lives behind a trusted adapter:

export type Document = {
  tenantId: string;
  documentId: string;
  revision: number;
  deleted: boolean;
  readers: readonly string[];
  text: string;
};

export type ReadSnapshot = (
  tenantId: string,
  documentIds: readonly string[]
) => Promise<readonly Document[]>;

The adapter contract matters more than its name. It must return a coherent, authoritative snapshot of content and access state, or reject the read. Fetching text from one stale replica and permissions from another does not satisfy that contract.

In a real application, this might be a database query that joins documents and current grants under an explicitly chosen transaction isolation level. Here it is an in-memory fixture, so we can inspect the behavior without hiding it behind infrastructure.

The authenticated server supplies the tenant and user. The model does not get to choose either value.

The full implementation validates candidate IDs and positive integer revisions, caps each request at 100 candidates, and rejects conflicting revisions for the same document.

After the snapshot, the core decision is small:

for (const doc of snapshot) {
  if (seen.has(doc.documentId)) {
    throw new Error("Duplicate snapshot row");
  }
  seen.add(doc.documentId);

  if (doc.tenantId !== principal.tenantId) continue;
  if (!refs.has(doc.documentId)) continue;
  if (doc.deleted || !doc.readers.includes(principal.userId)) continue;
  if (doc.revision !== refs.get(doc.documentId)) continue;

  sources.push({
    documentId: doc.documentId,
    revision: doc.revision,
    text: doc.text
  });
}

Notice where the text comes from: the current record, after the checks.

We do not ask the model to ignore revoked documents. We do not retrieve old text and hope a system prompt handles it correctly. The code building model context decides which sources can enter.

Azure AI Search's document-level access documentation describes enforcing permissions during query execution. Keep those retrieval filters. This gate adds a check at a later boundary, including when cached retrieval bypasses a new search.

It does not make cross-tenant retrieval acceptable. Prevent unauthorized retrieval upstream too, especially before logging, tracing, or reranking any content.

Suppose the cache refers to revision 7, but the current document is revision 9.

Why not substitute revision 9 and keep going?

Because the retrieval score belonged to revision 7. Revision 9 might discuss a different policy. Its relevance has not been established by that old result.

This example drops the candidate. The caller can run fresh retrieval if it needs more evidence.

That is a deliberate recall tradeoff. A safer result can be a smaller result.

There is another trap here: delete and recreate.

If you delete handbook at revision 7 and recreate it at revision 7, the old reference can accidentally match the new document. Revisions must never be reused for the same identity. A durable system can pair an immutable document incarnation ID with a monotonically increasing content version.

Our fixture recreates the handbook at revision 9. The stale revision-7 reference stays rejected.

The demo deliberately keeps its retrieval result unchanged while the underlying document changes:

before deletion: 1
after deletion: 0
after recreation, stale cache: 0
after fresh retrieval: 1

Those numbers count sources admitted to context. They are not model accuracy scores.

The repository has 17 passing tests. They cover authorized content, deletion, revoked access, a wrong tenant, a stale revision, an unexpected document, missing records, an authority outage, duplicates, conflicting revisions, recreation, fresh retrieval, malformed revisions, request bounds, empty retrieval, and a missing principal.

The important outage test looks like this:

await assert.rejects(
  buildContext(principal, [candidate], async () => {
    throw new Error("offline");
  }),
  /offline/
);

If the authority is unavailable, the operation fails. The caller must preserve that behavior. A catch block that falls back to yesterday's text would undo the entire design.

Run it locally:

git clone https://github.com/bobbyhalljr/tiny-rag-revocation-gate.git
cd tiny-rag-revocation-gate
npm test
npm run demo

I tested the code on Node 25.6.0 using synthetic fixtures. It needs no dependencies. Node 22.18 or later can execute this erasable TypeScript syntax directly. Native type stripping does not type-check the program, so execution tests and a TypeScript compiler check are separate things.

This gate checks a snapshot. It cannot reach backward and remove content already sent to a model.

A permission change after snapshot acquisition is also outside this example's guarantee. If the product requires a stricter revocation boundary, define when revocation becomes effective and coordinate admission with that boundary. A second check can narrow a race window, but it does not magically make a database read and a remote model request atomic.

Generated answers need attention too. If you cache the final answer, this source gate will never run on an answer-cache hit. Track the answer's source dependencies and revalidate them, or bypass that cache when you cannot establish current access.

Deleting vectors is also different from deleting every copy. Logs, traces, backups, provider retention, and stored conversations are separate systems with their own handling. This little gate makes no complete-erasure or compliance claim.

One useful operational measure is how many candidates get rejected for stale state. A sudden rise can point to a broken invalidation path. Avoid recording rejected document bodies in those diagnostics.

The code is intentionally small because the lesson belongs in the application boundary, where you can test it.

A cache can save the cost of finding a document.

It should not get to decide whether that document still belongs in the conversation.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @tiny-rag-revocation-gate 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-rag-cache-can-r…] indexed:0 read:6min 2026-10-27 · —