Prompt
I’m building search for private documents with row-level security — what architecture makes sense?
Latest observation
A good architecture for search over private documents with row-level security (RLS) is usually:
Recommended pattern: index for retrieval, authorize at query time
Keep a search index for fast retrieval, but never treat it as the source of truth for access. Enforce permissions with a security filter derived from your app’s authorization model.
High-level flow
-
Ingest documents
- Store the canonical document in your primary database/object store.
- Extract searchable text, metadata, embeddings, etc.
- Index document content into your search system.
-
Attach security metadata to each indexed record
- Examples:
tenant_idowner_user_idallowed_group_idsclassificationproject_id
- Keep the metadata simple and queryable.
- Examples:
-
At search time
- Authenticate the user.
- Compute their effective permissions:
- tenant
- groups
- roles
- explicit shares
- Inject an RLS filter into the search query so only authorized documents can match.
-
Post-filter again if needed
- If the search engine returns snippets/highlights, optionally re-check authorization before rendering.
- This is a defense-in-depth measure.
Common architectures
1) Search engine with built-in filter support
Examples: Elasticsearch/OpenSearch, Meilisearch, Typesense, Solr.
You index document content plus security fields, then query with filters like:
tenant_id = current_tenantgroup_id IN user_groupsvisibility = public
This is often the simplest and fastest approach.
Pros
- Fast
- Mature
- Simple operationally
- Works well for keyword search and faceting
Cons
- Complex ACLs can make filters large
- Fine-grained per-document permissions can get tricky
- Need careful design to avoid filter explosion
2) Hybrid: search engine + authorization service
Use:
- Search index for candidate retrieval
- Auth service / ACL store for access checks
Query:
- Search returns top N candidate doc IDs
- Authorization service checks which IDs the user may see
- Final result set is intersection
Pros
- Very flexible permission model
- Handles dynamic sharing and complex ACLs
- Search index stays simpler
Cons
- Extra latency
- More round trips
- Harder to scale if candidate sets are large
This works well when permissions are highly dynamic or complex.
3) Precomputed per-tenant or per-principal indexes
Build separate indexes:
- by tenant
- by department
- sometimes even per customer
Pros
- Very strong isolation
- Simple queries
- Easy to reason about
Cons
- Index sprawl
- More expensive to maintain
- Not practical if users belong to many overlapping ACL sets
This is best for strong tenant isolation, especially SaaS multi-tenancy.
Best practice for RLS
Prefer “security trimming” using indexed permission attributes
Instead of checking every document against an ACL store at query time, store enough permission info in the index so the search system can apply a filter efficiently.
Typical patterns:
- Tenant boundary
tenant_id = X
- Group-based access
allowed_group_ids contains any of user_group_ids
- Direct sharing
shared_with_user_ids contains user_id
- Role/classification
classification <= user_clearance
For many systems, this is enough.
Key design decision: how to model permissions
Option A: Denormalize permissions into the index
Store all searchable permission tokens on the document.
Example:
{
"doc_id": "123",
"tenant_id": "t1",
"allowed_principals": ["u:7", "g:42", "g:99"],
"content": "..."
}
Query by matching any principal the user has.
Good when the ACL set is moderate.
Option B: Use relationship tables and expand at query time
Keep permissions in relational tables:
document_acl(doc_id, principal_type, principal_id)user_groups(user_id, group_id)
At query time:
- resolve user’s principals
- pass them into search filter
Good when authorization is already modeled relationally.
Option C: Materialize effective access lists
Precompute which docs each user can see.
Good for small systems, but usually not scalable if users/documents change often.
Security pitfalls to avoid
1) Don’t rely on the search index alone as a trust boundary
Always validate permissions somewhere authoritative. The index can be stale.
2) Be careful with snippets/highlights
A search result can leak sensitive terms even if the full doc is later denied. Ideally:
- filter before retrieval
- and only generate snippets for allowed docs
3) Avoid “filter after ranking” if possible
If the search engine ranks unauthorized docs and you drop them later, users may infer hidden content from result counts or ranking behavior.
4) Watch cache poisoning
If you cache search results, cache by:
- query
- tenant
- effective permission context Not just by query text.
5) Consider deletes and revocations
Revoked access should propagate quickly:
- index updates
- permission cache invalidation
- TTLs on auth caches
Recommended architecture for most SaaS apps
If I had to choose a default:
Use:
- Primary store for canonical docs and ACLs
- Search engine with indexed security fields
- Auth service to compute effective permissions
- Query-time security filters
- Periodic reindex / near-real-time updates
Data model example
Document index:
{
"doc_id": "d1",
"tenant_id": "t1",
"title": "Budget plan",
"body": "...",
"allowed_group_ids": ["g1", "g2"],
"shared_with_user_ids": ["u9"]
}
Search query:
{
"query": {
"bool": {
"must": [
{ "multi_match": { "query": "budget", "fields": ["title", "body"] } }
],
"filter": [
{ "term": { "tenant_id": "t1" } },
{
"bool": {
"should": [
{ "terms": { "allowed_group_ids": ["g1", "g8"] } },
{ "terms": { "shared_with_user_ids": ["u7"] } }
],
"minimum_should_match": 1
}
}
]
}
}
}
When to use each approach
Use simple indexed filters if:
- permissions are mostly tenant/group based
- search must be fast
- you want lower operational complexity
Use a hybrid ACL check if:
- permissions are highly granular
- sharing changes often
- you need exact correctness over latency
Use separate indexes if:
- tenant isolation is paramount
- documents should never co-reside logically
- you have a manageable number of tenants/indexes
Practical recommendation
For most private-document search systems:
- Separate tenant data
- Index permission tokens in the search engine
- Apply query-time security filters
- Recheck authorization before displaying content
- Invalidate quickly on permission changes
That gives you a good balance of:
- performance
- correctness
- maintainability
If you want, I can also sketch:
- a Postgres + OpenSearch reference architecture,
- a vector search + RLS design,
- or a multi-tenant ACL schema for this.