Why we don't use a separate vector database for enterprise RAG, but force everything onto PostgreSQL/pgvector with row-level security
Permissions belong in the same query as the similarity search – not in a filter afterwards.
The real problem is not search
Every RAG demo works with a vector database. The question enterprise projects fail on is a different one: may the person asking right now see this paragraph at all? And: can we still prove it two years from now?
A separate vector database creates two data stores with two permission models. Documents, versions and access rights live in PostgreSQL or the DMS, the embeddings somewhere else. Synchronisation becomes a security boundary – and that boundary is visible in no data model.
The decision
ATF Search stores chunks, embeddings, the full-text index, document versions and access rights in one PostgreSQL instance with pgvector. The permission check is part of the same SQL query as the vector search. There is no state in which a hit is loaded first and “filtered away” later.
-- schematic: similarity, full text and permission in one query
SELECT id, content, 1 - (embedding <=> $1) AS score
FROM chunks
WHERE collection = $2
AND (allowed_users @> ARRAY[$3] OR allowed_groups && $4)
ORDER BY embedding <=> $1
LIMIT 20;
The identity ($3, $4) comes from the verified token – never from a field the client sends along. Neither a model nor a front end can write itself permissions.
For Runtime’s tenant data – executions, cost, provider keys, PII vault entries – row-level security comes on top. Every transaction sets app.current_tenant transaction-locally; the tenant_isolation policy is forced on the tables (FORCE ROW LEVEL SECURITY). Even a query that lacks WHERE tenant_id = … then returns no foreign rows.
cd apps/runtime
bun run scripts/cli.ts rls-setup # show the plan (dry run)
bun run scripts/cli.ts rls-setup --confirm # create roles, grants, policies
TENANT_RLS_ENABLED=true TENANT_RLS_REQUIRED=true
Cross-tenant operator views use their own connection with their own role (DATABASE_OPERATOR_URL). The production gate refuses to start when RLS is on but that separation is missing.
What you get
- One source of truth for permissions. If someone may not see a document in the DMS, they will not get it through the AI either. There is no second ACL that can go stale.
- Hybrid without an extra system. Dense vectors and PostgreSQL full text are combined with reciprocal rank fusion, optionally with SPLADE sparse vectors and reranking.
- Transactions across text and vector. A new document version is switched atomically. There is no window in which old embeddings point at new text.
- Erasure you can prove. A GDPR erasure request hits text, embedding and metadata in the same database – and ends in a signed receipt.
- Operations your IT already knows. Backup, point-in-time recovery, monitoring, encryption at rest: all standard PostgreSQL.
What it costs – honestly
pgvector is not the fastest engine in the world at billions of vectors. ATF counters with half precision (halfvec), quantisation, bounded candidate sets and score-keyset pagination. For typical enterprise sizes – millions of chunks, not billions – this is more than enough. Whoever truly needs web scale usually has a different permission model as well.
In ATF, RLS is a deliberate onboarding step, not a silent default: the roles and credentials belong to the operator. Application-side tenant filtering stays mandatory – RLS is defence in depth, not permission to drop predicates.
The consequence for your project
When an auditor asks why employee A got an answer from document B, we show an SQL policy and a trace – not synchronisation logic between two systems.
This decision in your architecture?
Abillify helps companies build, integrate and operate audit-proof AI platforms based on ATF.