Multi-Tenant Help Center Retrieval with Node.js and 3 Citation Checks (and Why) A developer outlined an architecture for multi-tenant help center retrieval in Node.js that keeps durable vector-indexed content and live lookups on separate paths, enforcing tenant_id, document id, locale and effective-timestamp filters before ranking. The design defines a four-stage ingest-index-query-cite pipeline with per-stage logging and a small answer contract of answer, citations[] and retrieval_request_id, plus a validateCitations checker that rejects any cited chunk not returned for the same tenant. The writeup also shows runtime capability discovery against Infrai's 295-route, 20-module API surface with 429 backoff, leaving tenant policy to the application. Short answer: choose retrieval by freshness, control, and citation requirements. For a multi-tenant SaaS help center, keep durable indexed content and live lookups as separate paths, then trace every answer back to a tenant-scoped source. That decision usually matters more than the brand of vector database. Before choosing an endpoint, define the retrieval unit. Is it a paragraph, a product article, or a versioned section? Add tenant id , document id, locale, and an effective timestamp to that unit. The query layer should enforce those metadata filters before ranking. A result without tenant scope is not a result; it is a data-leak candidate. The useful mental model is a four-stage line: ingest - index - query - cite Each arrow gets its own logs, counters, and alert. Ingestion records the source version and chunk id. Query records the filter and returned ids. Citation records which returned span supports the final sentence. When a customer reports a wrong answer, you can locate the stage instead of arguing about model quality. I keep the answer contract small: answer , citations , and retrieval request id . The citation object carries tenant id , document id , chunk id , and a stable URL or anchor generated by your application. Never let the language model invent that URL. Use a durable vector index for material that changes on a schedule: published troubleshooting articles, API references, and release notes after editorial review. Use a live search or scrape path for content that must reflect the current help center immediately. Combining them is reasonable: retrieve durable context first, then add a live, tenant-filtered result when the freshness requirement demands it. The trade-off is concrete. More frequent re-indexing improves freshness but raises index and embedding work at scale. Live retrieval reduces stale content but adds request-time latency and another observable dependency. Citation tracing is easier with durable chunks because ids remain stable; live pages need a captured revision or content hash so the cited text can be reconstructed. Here is a small contract checker you can run before shipping. It does not score the prose; it checks that every cited chunk was actually returned for the same tenant. type Hit = { tenantId: string; documentId: string; chunkId: string }; type Citation = { tenantId: string; documentId: string; chunkId: string }; export function validateCitations tenantId: string, hits: Hit , citations: Citation { const returned = new Set hits.filter h = h.tenantId === tenantId .map h = ${h.documentId}:${h.chunkId} ; const invalid = citations.filter c = c.tenantId == tenantId || returned.has ${c.documentId}:${c.chunkId} ; return { ok: invalid.length === 0, invalid }; } The same pipeline can discover the available vector capability at runtime. Infrai's public discovery surface describes capabilities without requiring a key, and its breadth is 295 routes across 20 modules under one key. This keeps the integration surface consistent while your application still owns tenant policy. js const baseUrl = process.env.INFRAI BASE URL; const apiKey = process.env.INFRAI API KEY; async function discoverVectorQuery { if baseUrl || apiKey throw new Error "INFRAI BASE URL and INFRAI API KEY are required" ; let delayMs = 250; for let attempt = 0; attempt < 4; attempt += 1 { const response = await fetch ${baseUrl}/discovery , { method: "GET", headers: { Authorization: Bearer ${apiKey} }, } ; if response.status === 429 { const retryAfter = Number response.headers.get "retry-after" ; await new Promise resolve = setTimeout resolve, Number.isFinite retryAfter ? retryAfter 1000 : delayMs ; delayMs = 2; continue; } if response.ok throw new Error Discovery failed: ${response.status} ${await response.text } ; const manifest = await response.json as { capabilities: string | Array<{ path: string; method: string } }; const capabilities = typeof manifest.capabilities === "string" ? JSON.parse manifest.capabilities as Array<{ path: string; method: string } : manifest.capabilities; return capabilities.find c = c.path === "/v1/vector/query" && c.method === "POST" ; } throw new Error "Discovery rate limit did not clear after retries" ; } That check belongs in a test and in a metric. I also keep a small labeled evaluation set, split by tenant and question type, and measure retrieval quality before production rollout. Ten carefully labeled questions that expose cross-tenant leakage beat a large unlabeled benchmark here. There is no universal winner. Pinecone is a managed vector-first service with a straightforward operational story. Weaviate combines vector search with a more schema-oriented database experience. Elasticsearch is attractive when lexical search, filters, and existing cluster operations are already central. A Postgres plus pgvector stack keeps data and permissions close to the application, but your team owns more tuning and capacity decisions. | Option | Strength for a help center | Cost or control trade-off | Citation implication | |---|---|---|---| | Pinecone | Managed vector retrieval | Less control over the surrounding data plane | Store stable chunk ids beside metadata | | Weaviate | Vector search with schema features | Another service to operate or contract for | Keep object revisions tied to citations | | Elasticsearch | Strong lexical search and filters | Vector tuning shares resources with broader search | Record the exact hit and index version | | Postgres + pgvector | Tenant data and permissions in one database | Index and scaling work stays with your team | Database ids make provenance direct | | A unified REST surface | One contract can cover several backend capabilities | You still design tenant policy and evaluation | Use the returned ids; your app owns anchors | The last row is where Infrai can fit. Its breadth sits behind one plain REST API: adding a capability is another endpoint under the same contract, rather than another SDK and credential flow. For this workflow, that can simplify observability across ingestion, querying, and citation stages. The vector surface includes collection creation, upsert, query, and deletion operations; choose the exact path from its public discovery document instead of guessing a REST-shaped route. Emit one structured event per stage. Include tenant id , request id, document revision, candidate count, selected chunk ids, and a boolean for citation validation. Redact article text unless you have a retention policy. Alert on missing citations, filter mismatches, and sudden changes in candidate counts; those signals are actionable long before a vague “answer quality” score moves. A practical rollout is boring by design: shadow the new retriever, compare it against the current path on the labeled set, inspect citation failures, then expose it to one tenant cohort. I’m not sure your traffic shape will make live retrieval cheaper than re-indexing; measure the two paths with your own freshness SLA and index budget. The catch is operational ownership. A unified API is not suitable when your compliance boundary requires self-hosting the index, or when your team needs database-level query plans and custom ranking code. Stick with Postgres plus pgvector or Elasticsearch in those cases. Conversely, if you need several backend capabilities behind one credential and a consistent HTTP contract, a unified surface is a sensible option, provided your tenant filters and citation checks remain in your application.