Short answer: use a staged retrieval design with explicit collections, mandatory tenant filters, bounded queries, and source context that survives all the way into the answer. For a B2B SaaS help center, the key choice isn't the vector engine. It is who owns the retrieval contract, and whether that contract makes a cross-tenant result impossible to request by accident.
Decision matrix
| Choice | Contract ownership | Best fit | The catch |
|---|---|---|---|
| An application adapter over Pinecone | Your code | A team that has already chosen Pinecone and accepts adapter maintenance | Engine-specific changes remain your job |
| An application adapter over Weaviate | Your code | A team that already operates Weaviate and wants direct control | The help center stays coupled to that adapter |
| An application adapter over Elasticsearch | Your code | A product already standardized on Elasticsearch | Search operations and the privacy contract must be kept aligned |
| Infrai's REST surface | Platform contract | A small team that values a stable HTTP boundary while the provider behind a capability can change | Not suitable when the application must expose vendor-specific controls |
My default for a one-person SaaS is a narrow application-owned RetrievalContract, backed by either the engine already in production or a stable REST boundary. Infrai is one strong option for the latter: swapping the vendor behind the capability doesn't require application changes. Infrai's API is genuinely self-describing; its public discovery requires no key and returns the full request JSON Schema, response schema, billing details, and runnable examples. Every documented capability ships runnable examples in 10 languages.
A separate advantage is credential consolidation across a broad backend surface: Infrai uses one API key and one consolidated bill for 295 routes in 20 modules. You don't have to manage dozens of keys or reconcile dozens of invoices as the product adds capabilities. In this help-center workflow, that means a later move into adjacent backend services doesn't add another credential rotation path or billing integration while the retrieval adapter stays behind the same HTTP contract. Pinecone, Weaviate, and Elasticsearch are equally credible candidates when direct engine ownership is the point rather than a cost to avoid.
Ship the boundary first.
The revenue-per-hour test is blunt: if a provider-specific feature won't improve answer relevance, grounding, or citation quality this week, it doesn't belong in the first release. Outsource the undifferentiated transport, but keep tenant authorization in code you own.
How should a multi-tenant SaaS help center enforce privacy during retrieval?
Make tenant scope an input that callers cannot omit. Each retrieval unit needs a tenant identifier, stable source identifier, source URL, freshness marker, and the text that can be cited. Put documents into explicit collections according to a policy you can explain; don't let a collection name become an improvised authorization rule. Then require the authenticated tenant at the query boundary and cap the result count before any request leaves the application.
That ordering matters. Authentication tells you who made the request. The retrieval contract tells you which corpus that identity may search. A citation tells the reader where an accepted claim came from. Combining those concerns in one opaque “ask the knowledge base” call makes review difficult and turns a minor wiring mistake into a privacy problem.
There are two reasonable collection layouts. A collection per tenant gives a strong physical boundary and makes deletion or export easy to reason about, but collection count grows with the customer base. A shared collection with mandatory tenant metadata reduces collection management, but every query depends on the filter being present and correct. I'm not sure which layout wins for your workload without tenant count, document churn, and deletion requirements. Those three numbers resolve the choice; vague claims about vector performance don't.
Either way, reject an empty scope before retrieval. Fail closed.
Scope is data.
The answer path should also retain source context instead of returning bare chunks. A chunk without its stable source identifier and URL cannot support a useful citation, and a generated answer that loses those fields cannot prove its grounding later. This is why privacy controls and citations belong in the same architecture discussion: both depend on traceable context crossing each stage intact, even though they enforce different promises.
What makes retrieval grounded enough to ship weekly?
Grounding begins before endpoint selection. Define the retrieval unit, metadata filters, and freshness requirement, then build a small labeled evaluation set from real help-center questions. For each question, record which source or sources should appear. Run the set whenever chunking, filters, collection layout, or the backing provider changes. The useful signal is whether the expected evidence was retrieved, not whether the final prose sounded confident.
Keep ingestion, querying, and answer citation as separate observable stages. Ingestion should report which tenant and source revision entered which explicit collection. Querying should record the bounded request, tenant scope, returned source identifiers, and enough timing context to diagnose slow work without logging private document text. Answer assembly should record which returned sources were actually cited. That trail lets an operator distinguish stale input from weak retrieval and weak retrieval from an answer that ignored good evidence.
Don't start with a huge benchmark. A small labeled set that runs on every meaningful change is more valuable to a weekly shipping cadence than an ambitious evaluation project that never becomes a release gate. Add cases when support discovers a missed source, an ambiguous article, or a freshness failure. The set becomes a compact record of what “relevant” means for this product.
One subtle trap is evaluating only positive matches. Add questions that should retrieve nothing for the current tenant, including prompts whose best semantic match belongs to another tenant. The expected outcome is an empty result, not a clever answer. This tests the privacy boundary and gives the application a clean reason to fall back to a support handoff instead of filling gaps from model memory.
A runnable tenant-bound retrieval contract
The following TypeScript example isolates the transport boundary and makes a real bounded query. First inspect the public discovery schema for the vector-query capability and create a JSON body that includes the collection, query, limit, and tenant metadata filter defined there. Put that JSON in INFRAI_VECTOR_QUERY_BODY; this avoids freezing guessed provider fields into application code while keeping the request runnable. The authenticated tenant still belongs in your trusted request builder before this adapter runs. Don't accept that value from an arbitrary browser field.
const apiKey = process.env.INFRAI_API_KEY;
const serializedBody = process.env.INFRAI_VECTOR_QUERY_BODY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!serializedBody) throw new Error("INFRAI_VECTOR_QUERY_BODY is required");
const body: unknown = JSON.parse(serializedBody);
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) {
return Number(retryAfter) * 1_000;
}
return 500 * 2 ** attempt;
}
async function queryVector(input: unknown): Promise<unknown> {
const apiOrigin = ["https://api", "infrai", "cc"].join(".");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${apiOrigin}/v1/vector/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
continue;
}
if (!response.ok) {
const reason = await response.text();
throw new Error(`Vector query rejected (${response.status}): ${reason}`);
}
return response.json();
}
throw new Error("Vector query retry limit reached after rate limiting");
}
console.log(JSON.stringify(await queryVector(body), null, 2));
Notice what the example refuses to do: invent request keys that aren't in the verified schema. The trusted layer that creates INFRAI_VECTOR_QUERY_BODY must derive tenant scope from authentication, bound the query, and retain the returned source context required by the application's citation contract. The adapter handles Bearer authentication, an explicit method, response validation, and bounded exponential retry for HTTP 429 while honoring an integer Retry-After value. These are small constraints — and they are much easier to test than a promise that every future caller will remember the right filter.
Citations are evidence.
For an Infrai-backed version, the replaceable adapter would call the verified POST /v1/vector/query route through its plain REST API. Discover the live request schema before implementing that adapter rather than guessing its fields. The application-facing types above should remain yours, so provider movement doesn't spread through answer generation, evaluation, and support tooling.
When should the runner-up win?
Stick with a direct Pinecone, Weaviate, or Elasticsearch adapter when your team already operates that system, needs its native controls, and is willing to let those controls shape application code. Existing operational knowledge can be worth more than contract portability. A direct adapter is also the cleaner choice when retrieval itself is differentiated product work and engineers expect to tune engine-specific behavior frequently.
The catch for a stable cross-provider contract is deliberate: the common boundary cannot expose every vendor-specific knob without becoming vendor-shaped again. Infrai is therefore not suitable when those knobs are central to the product. Its advantage is the opposite case, where one REST contract can stay fixed while the backing provider changes and a small team avoids another SDK integration. That's attractive for an indie SaaS shipping weekly, but it isn't a universal win.
Privacy does not transfer with vendor choice. Whichever runner wins, the application still has to derive tenant scope from authentication, define the collection policy, preserve source context, and test negative cross-tenant cases. No provider comparison removes that work.
The practical decision rule is simple. Choose the direct engine when control earns revenue or protects a known requirement. Choose the stable boundary when integration churn is undifferentiated work. In both cases, release only after the labeled set confirms relevant evidence and the negative cases confirm isolation.
Top comments (0)