Designing Fresh, Tenant-Safe Retrieval for SaaS Help Centers (and Why I Chose One) A developer described a staged retrieval architecture for multi-tenant SaaS help centers that enforces mandatory tenant filters, document-version tracking and freshness contracts to prevent cross-tenant leaks and stale citations. The design separates ingestion, querying and citation into distinct stages with their own counters and dead-letter paths, and rejects requests missing a tenant filter rather than falling back to a default. The engineer also recommended labeling a small evaluation set of real questions, expected tenants and acceptable source versions to measure recall at a fixed k. Short answer: for a multi-tenant SaaS help center, use a staged retrieval architecture with explicit collections, bounded queries, and source context that can be traced back to a document version. Decide the retrieval unit and freshness contract before choosing a vector endpoint. I have been paged for missed jobs and duplicate deliveries. A help-center answer can fail in the same quiet way: the result belongs to the wrong tenant, or it quotes a page that was replaced yesterday. The incident lesson is boring but useful. Retrieval is a pipeline with failure boundaries, not a single similarity call. It failed. The concrete failure mode is easy to miss during a launch review. A PDF replacement lands while an embedding worker is still draining yesterday's queue. The query sees both versions, and a similarity score picks the older paragraph because it happens to share more words with the question. Meanwhile, a tenant filter is assembled in a different service and silently omitted for one code path. Nothing crashes; the answer is just confidently wrong. My runbook for this now starts with the retrieval contract: tenant must be present, is current must be true unless an incident explicitly permits history, and every candidate needs a document version that the citation stage can resolve. We then replay the question against the labeled set, inspect the candidate IDs, and only after that look at model wording. This order matters during an on-call shift because a polished answer can hide a bad retrieval decision for hours. Start with the answer a customer is allowed to see. For each chunk, store tenant id , document id , document version , source uri , heading path , chunk index , updated at , and an is current flag. The vector is only one field. A result without tenant and version context is not safe to cite. Collections should have an explicit ownership boundary. I prefer one logical collection per environment with a mandatory tenant id filter when the backend can enforce it; separate collections per tenant are reasonable when isolation policy or deletion workflows demand it. Either way, make a missing tenant filter a rejected request, not a best-effort default. Chunk around meaning, not a fixed character count. A short troubleshooting step can stand alone; a long API procedure may need a parent heading copied into every child chunk. Keep overlap small and deliberate. Your mileage may vary when PDFs contain tables or scanned text, so measure instead of treating a chunk size from a blog post as law. Freshness needs a number and an owner. For product status pages, minutes may be the contract. For a stable policy PDF, a daily rebuild may be enough. Record the crawl timestamp and document version in the payload, then reject stale candidates at query time. This makes re-ingestion observable and lets support explain why an answer changed. Separate ingestion, querying, and citation. Ingestion extracts PDF text, assigns a new version, embeds chunks, and upserts them. Querying applies tenant and freshness filters, retrieves a bounded top-k set, and logs request and result identifiers. Citation rendering receives those identifiers and links the answer to the exact source URI and heading. Each stage gets its own counters and dead-letter path. Before production, label a small evaluation set: real questions, expected tenant, acceptable source versions, and a few “no answer” cases. Measure recall at a modest k and inspect false positives by tenant. I started out thinking a larger k would cover missed facts; later I found that an unbounded candidate set mostly made citation review harder. Keep the query budget fixed, then tune chunking and filters against the labels. The preventative path can remain plain HTTP. Infrai is useful here when the team wants one REST API and no SDK lifecycle to manage; its vector surface provides the same explicit request boundary as the rest of the pipeline. The example below shows a bounded query and treats duplicate delivery as normal operational input. package main import "bytes" "encoding/json" "fmt" "io" "net/http" "os" "time" type queryRequest struct { Query string json:"query" TopK int json:"top k" Filter map string any json:"filter" } func main { key := os.Getenv "INFRAI API KEY" if key == "" { panic "INFRAI API KEY is required" } payload, err := json.Marshal queryRequest{ Query: "How do I rotate an API key?", TopK: 5, Filter: map string any{ "tenant id": "tenant 42", "is current": true, }, } if err = nil { panic err } client := &http.Client{Timeout: 10 time.Second} for attempt := 0; attempt < 3; attempt++ { baseURL := os.Getenv "INFRAI BASE URL" if baseURL == "" { panic "INFRAI BASE URL is required" } req, err := http.NewRequest http.MethodPost, baseURL+"/v1/vector/query", bytes.NewReader payload if err = nil { panic err } req.Header.Set "Authorization", "Bearer "+key req.Header.Set "Content-Type", "application/json" resp, err := client.Do req if err = nil { if attempt == 2 { panic err } time.Sleep time.Duration 1<