Skip to content

Writing

Adding RAG to a SaaS product that already has customers

Every RAG tutorial starts with an empty folder. Here's the version for a product that already has tenants, permissions and a database you can't casually restructure.

By Yash Mittal4 min read

Adding retrieval-augmented generation to an existing SaaS product differs from a greenfield build in four ways: the index must enforce tenant isolation at query time rather than after retrieval, ingestion must run incrementally against data that is already changing, the vector store should usually be pgvector inside the database you already operate rather than a new service, and the per-tenant running cost has to be modelled against your existing pricing before launch. The build is typically four to eight weeks.

Every RAG tutorial begins the same way: an empty directory, a folder of PDFs, a loop that embeds them. It's a genuinely good way to learn the concepts and almost useless as a guide to putting retrieval into a product that already has customers, permissions and a schema you can't casually restructure on a Tuesday.

Here's the version for that situation.

Decide what question it answers before you index anything

The failure mode specific to existing products is indexing everything, because everything is right there. You end up with a search box that can answer any question mediocrely, which nobody uses twice.

Pick one question your users currently answer by trawling through your interface. "What did we agree with this customer about renewal terms?" "Which of our policies covers this situation?" "Has this issue come up before, and what fixed it?" One question, one corpus, one measurable improvement. Expand after it works.

Tenant isolation is the first design decision, not the last

In a multi-tenant product, retrieval that returns another customer's data is not a bug, it's a breach notification. This has to be enforced structurally, and the choice is between three options.

ApproachWhen it fitsThe cost
Shared index, tenant_id pre-filterMost products. Hundreds to millions of tenants.One missing filter is a cross-tenant leak. Must be enforced in a single query layer, never left to callers.
Index per tenantTens of large tenants, or a contractual isolation requirement.Operational overhead grows linearly. Painful past a few hundred.
Separate database per tenantYou already run this model.Nothing new — you've already paid this cost elsewhere.
Pick by tenant count and isolation requirements. Most B2B SaaS products should start in the top row.
-- The filter is part of the search, not applied afterwards
SELECT id, content, 1 - (embedding <=> $1) AS score
FROM document_chunks
WHERE tenant_id = $2
  AND deleted_at IS NULL
ORDER BY embedding <=> $1
LIMIT 20;

CREATE INDEX ON document_chunks
  USING hnsw (embedding vector_cosine_ops)
  WHERE deleted_at IS NULL;
pgvector with the tenant filter inside the query. The partial index keeps it fast as the table grows.

Use the database you already have

The default advice is a dedicated vector database. For a team adding retrieval to an existing product, that advice is usually wrong, and the reason has nothing to do with benchmarks.

If you're already running Postgres, pgvector means your embeddings live in the same database as the rows they describe. Which means a document deleted in a transaction has its chunks deleted in the same transaction. No sync job, no eventual consistency, no window in which a deleted contract is still answerable. Tenant filtering reuses the row-level security you've already got, and your backups already cover it.

A dedicated vector store becomes the right answer past roughly ten million chunks, or when the embedding workload starts competing with transactional queries for the same connection pool. Both are good problems and neither is a launch problem. Start in Postgres; you can move later, and you probably won't need to.

Ingestion has to be incremental from day one

The greenfield tutorial does a one-off import. In a live product, data is changing while you index it, and the three cases that matter are the ones a batch script handles worst.

  1. 01Creation. New document, new chunks, embedded and indexed — ideally in a background job so a user's save doesn't wait on an embedding API.
  2. 02Update. Re-chunk and re-embed the changed document, then replace its chunks atomically. Half-updated documents produce answers stitched from two versions, which is worse than either.
  3. 03Deletion. The one that gets missed. A deleted document whose chunks survive means your assistant quotes a contract the customer terminated. Delete on the same transaction boundary as the source row.

Backfill the existing corpus with a throttled job, not a single pass — embedding APIs rate-limit, and a full re-embed of a large corpus is a real cost. Track progress per tenant so you can enable the feature for customers whose data is ready.

Hybrid search, because your users type identifiers

This matters more in an existing product than in a demo, because real users search the way they talk about your product: invoice numbers, ticket IDs, SKUs, surnames, version strings. Vector search is bad at all of those — it returns semantically similar text that contains none of them.

Run Postgres full-text search and vector search in parallel, fuse the results with reciprocal rank fusion, then re-rank the fused set with a cross-encoder. Re-ranking is typically the single biggest accuracy gain in the pipeline and the one most often skipped, because the pipeline appears to work without it.

Cost, per tenant, before you launch

A feature that costs $0.04 per query is free at pilot scale and a margin problem at ten thousand users. Model it against your actual pricing before release rather than after.

ComponentNaiveTuned
Embedding (ingest + re-embed)$2 – 8$1 – 3
Retrieval computeNegligibleNegligible
Generation tokens$15 – 60$4 – 15
Per tenant, per month$17 – 68$5 – 18
Illustrative monthly cost per active tenant. Ratios matter more than the absolute figures.

The three levers, in order of effect: trim retrieved context (most teams pass far more than the model needs), cache the stable prefix of the prompt, and route straightforward queries to a smaller model. Together these routinely cut generation spend by more than half.

Measure retrieval separately, and before generation

The instinct when answers are poor is to rewrite the prompt, because the prompt is the visible part. It's usually the wrong lever — if the right passage was never retrieved, no wording recovers it.

Take fifty real questions from your support queue with the passages that should answer them, and score retrieval on recall@k before touching generation. Only once the right material is reliably being found is it worth tuning what the model does with it. There's more on both stages in the retrieval section of our AI integration page.

Ship it to one tenant first

Behind a per-tenant flag, to a customer who'll tell you when it's wrong. Log every query with the retrieved chunk ids, whether the user acted on the answer, and whether they rephrased and asked again. Rephrasing is the most useful negative signal you'll get and the easiest to capture.

Those logged queries become your next eval set, which is the point at which the feature starts improving on evidence rather than on opinion.

This is what we do. How we build RAG systems.

How we build RAG systems

FAQ

Related questions

Do we need a vector database, or is pgvector enough?

pgvector is enough for the overwhelming majority of SaaS products, and it's usually the better choice — embeddings live in the same transaction boundary as the source rows, so deletes and updates stay consistent with no sync job. Consider a dedicated vector store past roughly ten million chunks, or when embedding queries start competing with transactional traffic for connections.

How do we stop RAG leaking data between tenants?

Enforce the tenant filter inside the retrieval query, never as a post-filter on results, and put that query behind a single data-access layer that no caller can bypass. Then test it explicitly: an eval case that asks tenant A a question only answerable from tenant B's data, which must return nothing. It's the one test worth running on every deploy.

How long does it take to add RAG to an existing product?

Four to eight weeks for a production feature over one corpus, including ingestion, hybrid retrieval, re-ranking, citations, tenant isolation and an eval suite. The variable is rarely the retrieval code — it's how clean the source data is and how well the permission model is already expressed in the database.

Should embedding happen synchronously when a user saves a document?

No. Embedding APIs add latency you don't control and fail in ways a save shouldn't. Write the row, queue the embedding job, and let search treat not-yet-indexed documents as simply absent. Show indexing status in the interface if the delay is user-visible — that's a much better experience than a save that occasionally takes four seconds.

Keep reading

More on this

Article

Can you combine RAG and fine-tuning? Yes, and usually you shouldn't yet

The comparison is usually framed as a choice. It isn't — they solve different problems. Here's which problem you actually have, and what combining them costs.

Article

The evals we build before shipping an LLM feature

Not a survey of eval tooling. The actual suite we build on client projects, why it's small, and which scoring methods we don't trust.

Next step

Want this applied to your situation?

Articles generalise. A 45-minute call doesn't — tell us what you're actually dealing with and we'll be specific.

Start a projectRead more articles