Documentation Index
Fetch the complete documentation index at: https://meilisearch-6b28dec2-add-federated-search-demo.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Combine foreign filters with tenant tokens to implement fine-grained, role-based access control (RBAC). Users see only documents they’re authorized to access based on their roles and team memberships.
How it works
Core concept: Create a separate access control table that defines which users and teams can access which documents. Use foreign filters to enforce these permissions at query time.
Tenant tokens carry user identity:
{
"sub": "jeremy@meilisearch.com",
"teams": ["product", "engineering"]
}
Foreign filters enforce the rules:
_foreign(access, user = "jeremy@meilisearch.com" OR teams IN ["product", "engineering"])
Only documents where the access table grants permission are returned.
Data structure
Access control table
Create a separate “access” index to define permissions:
[
{
"id": "access_1",
"document_id": "doc_internal_memo_1",
"user": "jeremy@meilisearch.com",
"teams": ["product", "engineering"],
"roles": ["viewer", "editor"]
},
{
"id": "access_2",
"document_id": "doc_internal_memo_1",
"teams": ["finance"],
"roles": ["viewer"]
},
{
"id": "access_3",
"document_id": "doc_public_post_1",
"teams": ["*"],
"roles": ["viewer"]
}
]
Main documents
Documents reference the access control table:
[
{
"id": "doc_internal_memo_1",
"title": "Q4 Product Roadmap",
"content": "...",
"access_id": "access_1"
},
{
"id": "doc_public_post_1",
"title": "Welcome to our blog",
"content": "...",
"access_id": "access_3"
}
]
Setting up relationships
- Create access control index with documents defining who can access what
- Add foreign key to your main index pointing to access table:
{
"foreignKeys": [
{
"fieldName": "access",
"foreignIndexUid": "access"
}
]
}
- Configure filterable attributes on access table:
{
"filterableAttributes": [
"user",
"teams",
"roles"
]
}
Using tenant tokens with RBAC
The tenant token contains the authenticated user’s identity:
{
"sub": "jeremy@meilisearch.com",
"teams": ["product", "engineering"],
"exp": 1234567890
}
When the user searches, the application includes this token and constructs the filter:
curl \
-X POST 'MEILISEARCH_URL/indexes/documents/search' \
-H 'Authorization: Bearer TENANT_TOKEN' \
-H 'Content-Type: application/json' \
--data-binary '{
"q": "roadmap",
"filter": "_foreign(access, user = \"jeremy@meilisearch.com\" OR teams IN [\"product\", \"engineering\"])"
}'
Result: Only documents where the access table has an entry for Jeremy (direct user match) or for the “product” or “engineering” teams are returned.
Multi-level RBAC example
Combine user, team, and role filtering:
curl \
-X POST 'MEILISEARCH_URL/indexes/documents/search' \
-H 'Content-Type: application/json' \
--data-binary '{
"q": "sensitive",
"filter": "_foreign(access, (user = \"jeremy@meilisearch.com\" AND roles IN [\"editor\", \"owner\"]) OR (teams IN [\"product\", \"engineering\"] AND roles IN [\"editor\"]))"
}'
This returns documents where:
- Jeremy has editor or owner role, OR
- Product/engineering teams have at least editor role
Handling wildcard access
For public documents, set teams = ["*"] in the access table:
{
"id": "access_public",
"document_id": "doc_public_announcement",
"teams": ["*"],
"roles": ["viewer"]
}
Filter to include public documents:
"filter": "_foreign(access, user = \"jeremy@meilisearch.com\" OR teams IN [\"product\", \"engineering\", \"*\"])"
-
Access table size: Each document’s access rules create entries. For 1000 documents with 10 team access rules each, you need ~10,000 access records.
-
Filter specificity: The foreign filter must match ≤ 100 access records. Design your access control structure to stay within this limit:
- Use team-based rules instead of per-user rules where possible
- Group documents by access level (public, internal, secret)
- Consider combining user + team checks:
(user = "..." OR (teams IN [...] AND roles IN [...]))
-
Denormalization trade-off: If RBAC queries regularly hit the 100-document limit, consider denormalizing permission fields directly into documents instead of using joins.
Security best practices
- Token validation: Always validate tenant tokens server-side before searching
- Immutable filters: Construct the filter on the server, never client-side
- Scope limitation: Limit token expiration and use short-lived tokens when possible
- Audit logging: Log access attempts for compliance and debugging
- Regular review: Periodically audit access control table entries to remove stale permissions
Example: Implementing on the server
import { Meilisearch } from 'meilisearch'
import jwt from 'jsonwebtoken'
const client = new Meilisearch({ host: 'http://localhost:7700', apiKey: 'ADMIN_API_KEY' })
async function searchDocuments(query, tenantToken) {
// 1. Validate token server-side
const user = jwt.verify(tenantToken, process.env.JWT_SECRET)
// 2. Construct filter from token claims
const teams = JSON.stringify(user.teams)
const filter = `_foreign(access, user = "${user.email}" OR teams IN ${teams})`
// 3. Search with filter
const results = await client.index('documents').search(query, { filter })
return results
}
Next steps
Tenant tokens
Learn how to generate and manage tenant tokens
Foreign filters
Understand foreign filter syntax and capabilities
Define relationships
Set up join relationships for RBAC