Build1 publisher2 min readPublished
A RAG design re-reads the user's department from Postgres before every vector search
A dev.to write-up keeps access rights in Postgres and vectors in Qdrant, with a department_id stamped into every chunk payload so the filter runs before the model sees any text. The token supplies the identity, and the department comes from Postgres.
The Engineer · Build desk

What happened
- A dev.to write-up sets out an enterprise RAG design that keeps Postgres as the source of truth for identities, access rights and file metadata while Qdrant handles vector search and context filtering.
- Ingestion loads PDFs with PyPDFLoader and splits them with LangChain's RecursiveCharacterTextSplitter so that chunks stop at paragraph boundaries before being upserted.
- Every Qdrant point stores the chunk text, a postgres_id linking back to the relational row, a chunk_index, and extra attributes such as department_id, all written into the payload at upsert.
- The write-up puts a Marketing user reaching HR payroll data above the problem of fitting multi-gigabyte datasets into a context window.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Anyone carrying department in token claims now has a stated choice: keep the claim and accept that access survives until the token expires, or add a relational read to the query path.
- constraint Because the ownership attribute is written at ingest, a permission change becomes an index write, and a reorg turns into a re-upsert job across every chunk of every affected document.
- exposure The guarantee is only as strong as the tagging: any vector that reaches the store without its ownership attribute sits outside the scheme the whole design leans on.
On each request the API takes the `sub` claim from the JWT for identity, then reads the user's current `department_id` from the relational store before a search is built [10]. "Access control must be deterministic and enforced at the database level," the author wrote [8]. Revocation depends on that read. A department baked into a signed claim keeps working until the token expires; a lookup on the request path applies a transfer out of HR to the next query. According to the write-up, "Relying on the relational store instead of stale claims inside the token allows for immediate revocation of access rights" [9].
The term the piece uses is ABAC, not RBAC, because the filter matches ownership attributes tagged onto every vector, with `department_id` as the example [11]. The attribute has to be present on every point at upsert time, and the ingestion code writes it into the payload there [6].
The chunking defaults cost something at ingest. `chunk_size=1000` with `chunk_overlap=150` leaves a stride of 850 characters, so a document yields roughly 18 percent more chunks than the same text split with no overlap [4][14]. Each chunk is a vector, a payload and, in the code as published, its own embedding request: `embed_query` is called inside the loop over chunks rather than batched [7].
The stated reason for going granular is the "Lost in the Middle" effect, where long inputs make models miss information sitting in the center of the prompt while cost and latency rise [12]. The target is 3 to 4 paragraphs carrying the answer in place of a 50-page PDF [13]. Whether 1000 and 150 get you there depends on the documents: the splitter is chosen so that chunks respect paragraph boundaries [5], so I would expect those settings to hold on prose with paragraphs near that length and to fall apart on tabular PDFs, where there is no paragraph boundary to respect. The write-up does not include retrieval or latency measurements at those settings, and it does not describe how ownership attributes are updated after ingest [15].
In my view the ordering in the piece is right, and the reason is mechanical. A retrieval miss returns a weak answer the user can see is weak. A filter miss returns HR's payroll paragraph in the model's voice, with a citation. "Security in RAG isn't just about hiding UI elements; it's about deterministic Vector Filtering," the author wrote [17].
What to watch
- Whether a follow-up publishes the Qdrant filter itself; the excerpt breaks off inside get_current_user before the search call.
- Any measurement of retrieval quality or query latency at chunk_size 1000 and overlap 150 on the author's own corpus.
- Whether the single department_id attribute holds up when one document has to be visible to two departments at once.