Skip to content

Version‑aware Retrieval‑Augmented Generation with Strapi and Revisionary

Learn how to create a RAG pipeline that queries Strapi content while honoring version history, using Revisionary to keep answers up‑to‑date and auditable.

By the Revisionary team · · 5 min read

photo of computer cables
Photo by Kvistholt Photography on Unsplash

Strapi can store your content, and Revisionary records every change. By exporting the latest version of each entry to a vector store, you can build a Retrieval‑Augmented Generation (RAG) system that always answers from the most recent approved content while keeping a full audit trail. The steps are straightforward:

  1. Install and configure Revisionary so every edit is versioned.
  2. Export the current version of the content you want to augment.
  3. Index those texts in a vector database, storing the associated Revisionary version ID.
  4. When a user asks a question, retrieve the most relevant vectors, fetch the exact version from Strapi, and feed the text to your LLM.
  5. If the content changes, re‑index only the affected entries; the version ID lets you trace which answer used which content.

Below is a complete, production‑ready guide.

1. Prerequisites

  • A Strapi 5 project with the Revisionary plugin installed (downloaded as a zip and enabled in config/plugins.ts).
  • Node 18+ and a PostgreSQL/MySQL/SQLite database (as required by Strapi).
  • Access to a vector database (e.g., Pinecone, Weaviate, or an open‑source solution like Qdrant).
  • An LLM endpoint that accepts a prompt and returns a completion (OpenAI, Anthropic, etc.).
  • Basic knowledge of JavaScript/TypeScript and async/await.

2. Install Revisionary and Verify Versioning

bash
# From your Strapi project root
npm install ./revisionary-plugin.zip   # the zip you purchased

Enable the plugin in config/plugins.ts:

js
module.exports = {
  // ...other plugins
  revisionary: {
    enabled: true,
    // optional: configure retention, audit privacy, etc.
  },
};

Start Strapi and confirm the Version history panel appears when you edit a content type. See the guide [/docs/everyday-use/history] for details.

3. Choose the Content to Augment

Not every collection needs to be searchable. Typical candidates are:

  • Blog posts
  • Product documentation
  • Knowledge‑base articles
  • Legal or policy pages

Mark those content types as versioned in Revisionary settings (Revisionary → Settings → Content types). This ensures every edit creates a new version that can be referenced later.

4. Export the Latest Version of Each Entry

Revisionary stores each version in the revisionary_versions table. The latest version of a record can be fetched via the Content API with the populate=revision query parameter, or by using a custom endpoint.

Here is a small Node script that pulls the newest version of every entry in a given collection:

js
import fetch from 'node-fetch';

const STRAPI_URL = 'https://cms.example.com';
const API_TOKEN = 'YOUR_STRAPI_API_TOKEN';
const COLLECTION = 'articles'; // change to your content type

async function fetchLatestVersions() {
  const res = await fetch(
    `${STRAPI_URL}/api/${COLLECTION}?populate=revision&pagination[pageSize]=1000`,
    { headers: { Authorization: `Bearer ${API_TOKEN}` } }
  );
  const { data } = await res.json();
  return data.map(entry => ({
    id: entry.id,
    versionId: entry.attributes.revision?.id, // Revisionary version ID
    title: entry.attributes.title,
    body: entry.attributes.body,
    locale: entry.attributes.locale,
  }));
}

fetchLatestVersions().then(console.log).catch(console.error);

5. Index Content in a Vector Store

Each document you index should include the text, metadata, and the Revisionary versionId. This lets you retrieve the exact version later and store a provenance record.

Below is an example using Pinecone (the API is similar for other providers):

js
import { PineconeClient } from '@pinecone-database/pinecone';
import { embed } from './my-embedding-service.js'; // your own embedding function

const pinecone = new PineconeClient();
await pinecone.init({ apiKey: process.env.PINECONE_API_KEY, environment: 'us-west1-gcp' });
const index = pinecone.Index('strapi-rag');

async function indexEntries(entries) {
  const vectors = [];
  for (const entry of entries) {
    const text = `${entry.title}\n\n${entry.body}`;
    const embedding = await embed(text); // returns Float32Array
    vectors.push({
      id: `${entry.id}-${entry.versionId}`,
      values: embedding,
      metadata: {
        strapiId: entry.id,
        versionId: entry.versionId,
        title: entry.title,
        locale: entry.locale,
      },
    });
  }
  await index.upsert({ upsertRequest: { vectors } });
}

fetchLatestVersions().then(indexEntries).catch(console.error);

The vector ID combines the Strapi entry ID and the Revisionary version ID (${id}-${versionId}) to guarantee uniqueness across versions.

6. Querying – From Question to Answer

A typical RAG request consists of three steps:

  1. Embed the user question.
  2. Retrieve the top‑k nearest vectors.
  3. Fetch the full versioned content from Strapi and build the prompt.
js
import { embed } from './my-embedding-service.js';
import fetch from 'node-fetch';

async function retrieveRelevantDocs(question, k = 5) {
  const queryVector = await embed(question);
  const queryRes = await index.query({
    queryRequest: { vector: queryVector, topK: k, includeMetadata: true },
  });
  return queryRes.matches.map(match => match.metadata);
}

async function buildPrompt(question, docs) {
  const context = docs
    .map(d => `Title: ${d.title}\nContent: ${d.body || ''}`)
    .join('\n\n---\n\n');
  return `Answer the question using only the provided context.\n\nContext:\n${context}\n\nQuestion: ${question}\nAnswer:`;
}

async function answerQuestion(question) {
  const docsMeta = await retrieveRelevantDocs(question);
  // Fetch the full versioned bodies from Strapi (if not already stored)
  const docs = await Promise.all(
    docsMeta.map(async meta => {
      const res = await fetch(
        `${STRAPI_URL}/api/articles/${meta.strapiId}?populate=revision`,
        { headers: { Authorization: `Bearer ${API_TOKEN}` } }
      );
      const { data } = await res.json();
      const version = data.attributes.revision?.id;
      // Ensure we have the correct version; Revisionary guarantees the versionId matches
      return {
        title: data.attributes.title,
        body: data.attributes.body,
        versionId: version,
      };
    })
  );
  const prompt = await buildPrompt(question, docs);
  // Send prompt to your LLM (example using OpenAI)
  const llmRes = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_KEY}` },
    body: JSON.stringify({ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }] }),
  });
  const { choices } = await llmRes.json();
  return choices[0].message.content;
}

answerQuestion('How do we handle user data deletion?').then(console.log);

The prompt explicitly tells the model to rely only on the retrieved context, preventing hallucinations about outdated content.

black and silver laptop computer
Photo by path digital on Unsplash

7. Keeping the Index Up‑to‑Date

Whenever an editor publishes a new version, Revisionary emits a lifecycle hook (afterUpdate or afterPublish). Use a Strapi webhook to trigger re‑indexing of just the changed entry.

  1. Create a webhook in Strapi admin → Settings → Webhooks → New webhook.
  2. Set the URL to an internal endpoint (e.g., /api/rag/reindex).
  3. Choose the event Entry Published for the versioned collection.

The webhook payload contains the entry ID and the new revision.id. Your endpoint can then:

  • Pull the latest version (as in section 4).
  • Upsert the new vector, overwriting the old ID (${id}-${oldVersion}) or deleting it first.
js
// Express‑style handler for the webhook
app.post('/api/rag/reindex', async (req, res) => {
  const { entry, model } = req.body; // Strapi sends these fields
  const latest = await fetchLatestVersionsForId(entry.id);
  await indexEntries([latest]); // re‑upsert single vector
  res.sendStatus(200);
});

Because the vector ID incorporates the version, the old vector remains searchable until you explicitly delete it. If you prefer a clean store, call index.delete with the old ID before upserting.

8. Auditing Answers – Provenance for Compliance

Regulators increasingly demand proof of where AI‑generated answers originated. With Revisionary you can store a provenance record alongside each LLM response:

js
async function answerWithProvenance(question) {
  const answer = await answerQuestion(question);
  const docsMeta = await retrieveRelevantDocs(question);
  const provenance = docsMeta.map(m => ({
    strapiId: m.strapiId,
    versionId: m.versionId,
    title: m.title,
  }));
  // Persist to your own audit table or to Revisionary's audit log via API
  await fetch(`${STRAPI_URL}/api/audit-logs`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ question, answer, provenance }),
  });
  return answer;
}

The audit log can later be exported for legal review, satisfying the checklist in [/blog/ai-regulation-for-publishers-using-strapi-compliance-checklist].

9. Security and Access Control

  • Authentication: Protect the RAG endpoints with the same JWT strategy you use for the Strapi API.
  • Rate limiting: Apply the rate‑limit middleware described in [/blog/protect-strapi-nextjs-admin-api-from-brute-force] to avoid abuse of the embedding service.
  • Data privacy: If you store personal data, enable the Privacy settings in Revisionary to anonymize IPs in the audit log [/docs/settings/privacy].
  • Least‑privilege: Create a dedicated API token with only read permissions on the versioned collection and create permission on the audit‑log collection.

10. Full Example Project Structure

code
my-rag-app/
├─ src/
│  ├─ index.js            # entry point, starts Express server
│  ├─ rag/
│  │   ├─ fetch.js        # functions from sections 4‑6
│  │   ├─ embed.js        # wrapper around your embedding model
│  │   └─ llm.js          # call to OpenAI/Anthropic
│  └─ webhook/
│      └─ reindex.js      # webhook handler (section 7)
├─ .env                    # STRAPI_URL, API_TOKEN, PINECONE_KEY, OPENAI_KEY
└─ package.json

Deploy the server alongside your Strapi instance (same VPC) to keep latency low and avoid exposing the vector store directly to the public internet.

11. Testing and Monitoring

  • Unit tests: Mock the Strapi API and vector store to verify that the correct version ID is used.
  • Integration test: Publish a new version of an article, trigger the webhook, then query the same question and assert that the answer reflects the updated content.
  • Monitoring: Log the number of vectors upserted per day and the latency of the retrieval step. Alert if latency exceeds a threshold, as it may indicate a stale index.

12. When to Re‑index All Content

Full re‑indexing is rarely needed, but you may want to do it:

  • After a major schema change (new fields that affect the context).
  • When switching to a new embedding model.
  • Periodically, to prune vectors belonging to deleted versions (use Revisionary’s Trash to identify them).

A simple script can iterate over all versioned entries and rebuild the index in batches.

js
async function fullReindex() {
  const pageSize = 100;
  let page = 1;
  while (true) {
    const res = await fetch(`${STRAPI_URL}/api/articles?pagination[page]=${page}&pagination[pageSize]=${pageSize}&populate=revision`, {
      headers: { Authorization: `Bearer ${API_TOKEN}` },
    });
    const { data } = await res.json();
    if (data.length === 0) break;
    const entries = data.map(e => ({
      id: e.id,
      versionId: e.attributes.revision?.id,
      title: e.attributes.title,
      body: e.attributes.body,
      locale: e.attributes.locale,
    }));
    await indexEntries(entries);
    page++;
  }
}

Run this during a maintenance window; the vector store can handle bulk upserts.

Conclusion

By leveraging Revisionary’s versioning capabilities, you can build a RAG pipeline that:

  • Always answers from the latest approved content.
  • Provides a tamper‑evident audit trail linking each answer to a specific content version.
  • Reacts automatically to content updates via Strapi webhooks, keeping the vector index fresh without full re‑indexing.
  • Meets compliance requirements through built‑in provenance records and audit‑log privacy settings.

The result is a trustworthy AI‑augmented experience for both developers and content teams, combining the flexibility of a headless CMS with the power of modern LLMs.

TagsAIRAGStrapiRevisionaryLLMContent Management

The Revisionary team

Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.

This article was written with the help of AI.

All articles