Permission Mongo provides built-in multi-tenancy support, automatically isolating data by tenant ID. All queries, creates, updates, and deletes are scoped to the authenticated user’s tenant.
Overview
Multi-tenancy features:
Automatic tenant scoping - All operations filtered by tenant_id
JWT-based tenant identification - Tenant extracted from token claims
Per-collection tenant fields - Flexible field naming
Cross-tenant queries - Controlled access with admin roles
Tenant isolation enforcement - Prevents accidental data leaks
Configuration
Step 1: Define Tenant Fields
Configure tenant field mapping in schema.yaml:
settings :
default_tenant_field : tenant_id # Global default
collections :
products :
fields :
name :
type : string
price :
type : number
access :
tenant_field : tenant_id # Uses default
owner_field : created_by
invoices :
fields :
amount :
type : number
customer :
type : string
access :
tenant_field : company_id # Custom field name
owner_field : created_by
Source: /home/daytona/workspace/source/pkg/config/schema.go:131-136
Step 2: Include Tenant in JWT
Your authentication service must include tenant_id in the JWT:
{
"sub" : "user-123" ,
"tenant_id" : "acme-corp" , // Required for tenant scoping
"roles" : [ "manager" ],
"iat" : 1640000000 ,
"exp" : 1640003600
}
Step 3: Automatic Tenant Injection
Permission Mongo automatically adds tenant_id to documents:
# User creates a product
curl -X POST https://api.example.com/products \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Widget",
"price": 29.99
}'
{
"_id" : "507f1f77bcf86cd799439011" ,
"name" : "Widget" ,
"price" : 29.99 ,
"tenant_id" : "acme-corp" , // Automatically added
"created_by" : "user-123" , // From JWT sub claim
"created_at" : "2024-01-15T10:30:00Z" ,
"updated_at" : "2024-01-15T10:30:00Z"
}
Source: /home/daytona/workspace/source/pkg/api/handlers_batch.go:176-187
Tenant Isolation Patterns
Pattern 1: Automatic Filtering
All queries are automatically scoped to the user’s tenant:
# User queries products
GET /products?status=active
# Permission Mongo adds tenant filter:
db.products.find( {
tenant_id: "acme-corp", // From JWT
status: "active" // From query
})
Pattern 2: Tenant-Specific Owner Access
Combine tenant scoping with owner checks:
policies :
invoices :
accountant :
actions : [ read , update ]
when : |
doc.created_by == user.id
Query filter:
{
tenant_id : "acme-corp" , // Automatic
created_by : "user-123" // From policy
}
Pattern 3: Role-Based Tenant Access
Different roles see different scopes within tenant:
policies :
sales_leads :
sales_rep :
actions : [ read , update ]
when : |
doc.assigned_to == user.id
sales_manager :
actions : [ read , update , delete ]
when : |
doc.assigned_to in user.$subordinates
sales_director :
actions : [ read , update , delete ]
# No condition - sees all tenant data
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:224-286
Cross-Tenant Access
In some cases, admin users need to access multiple tenants:
Super Admin Role
Define a role that bypasses tenant filtering:
roles :
super_admin :
description : Cross-tenant administrator
policies :
products :
super_admin :
actions : [ read , update , delete ]
# No tenant filter - sees all tenants
Use super admin roles sparingly. They bypass tenant isolation and can access all data.
Tenant-Switching
For support scenarios, issue tokens with different tenant IDs:
{
"sub" : "support-user-456" ,
"tenant_id" : "customer-tenant" , // Switch to customer's tenant
"roles" : [ "support" ],
"original_tenant" : "support-org" , // Track original tenant
"support_ticket" : "TKT-1234"
}
Hierarchical Tenants
For complex organizational structures:
policies :
reports :
regional_manager :
actions : [ read ]
when : |
doc.tenant_id in user.claims.managed_tenants
JWT claims:
{
"sub" : "manager-789" ,
"tenant_id" : "hq-corp" ,
"roles" : [ "regional_manager" ],
"managed_tenants" : [ "us-west" , "us-east" , "us-central" ]
}
Tenant Data Isolation
Database-Level Isolation
For stronger isolation, use separate MongoDB databases:
server :
multi_tenancy :
mode : database # Each tenant gets own database
database_prefix : tenant_
Tenant “acme-corp” → Database “tenant_acme_corp”
Collection-Level Isolation (Default)
All tenants share collections with filtering:
server :
multi_tenancy :
mode : collection # Default - filter by tenant_id
Better performance, simpler management, adequate for most use cases.
Source: /home/daytona/workspace/source/pkg/config/schema.go:131-136
Tenant Scoping in Batch Operations
Batch operations respect tenant boundaries:
# Batch create
curl -X POST https://api.example.com/products/batch \
-H "Authorization: Bearer <token>" \
-d '{
"documents": [
{"name": "Product 1", "price": 10.00},
{"name": "Product 2", "price": 20.00}
]
}'
All documents get tenant_id:
[
{
"_id" : "..." ,
"name" : "Product 1" ,
"price" : 10.00 ,
"tenant_id" : "acme-corp" // Auto-added
},
{
"_id" : "..." ,
"name" : "Product 2" ,
"price" : 20.00 ,
"tenant_id" : "acme-corp" // Auto-added
}
]
Source: /home/daytona/workspace/source/pkg/api/handlers_batch.go:169-206
Testing Multi-Tenancy
Test Tenant Isolation
func TestTenantIsolation ( t * testing . T ) {
// Create documents for different tenants
tenant1Token := createToken ( "user-1" , "tenant-1" , [] string { "user" })
tenant2Token := createToken ( "user-2" , "tenant-2" , [] string { "user" })
// Tenant 1 creates product
resp1 := createProduct ( tenant1Token , map [ string ] interface {}{
"name" : "Product A" ,
"price" : 100 ,
})
assert . Equal ( t , 201 , resp1 . StatusCode )
productID := resp1 . JSON [ "_id" ].( string )
// Tenant 2 tries to read tenant 1's product
resp2 := getProduct ( tenant2Token , productID )
assert . Equal ( t , 404 , resp2 . StatusCode ) // Not found (due to tenant filter)
// Tenant 1 can read their own product
resp3 := getProduct ( tenant1Token , productID )
assert . Equal ( t , 200 , resp3 . StatusCode )
}
Test Cross-Tenant Admin
func TestSuperAdminAccess ( t * testing . T ) {
adminToken := createToken ( "admin-1" , "admin-tenant" , [] string { "super_admin" })
// Query all products across tenants
resp := listProducts ( adminToken , nil )
assert . Equal ( t , 200 , resp . StatusCode )
// Verify products from multiple tenants
products := resp . JSON [ "data" ].([] interface {})
tenants := make ( map [ string ] bool )
for _ , p := range products {
product := p .( map [ string ] interface {})
tenants [ product [ "tenant_id" ].( string )] = true
}
assert . Greater ( t , len ( tenants ), 1 ) // Multiple tenants
}
Audit Logging for Tenants
All tenant operations are automatically logged:
{
"timestamp" : "2024-01-15T10:30:00Z" ,
"tenant_id" : "acme-corp" ,
"user_id" : "user-123" ,
"action" : "create" ,
"collection" : "products" ,
"doc_id" : "507f1f77bcf86cd799439011" ,
"success" : true
}
Query audit logs per tenant:
GET /api/audit?tenant_id=acme-corp & start_date = 2024-01-01
Source: /home/daytona/workspace/source/pkg/api/handlers_batch.go:800-828
Common Patterns
Pattern: Tenant Admin
Allow tenant admins to manage tenant users:
policies :
users :
tenant_admin :
actions : [ read , update , delete ]
when : |
doc.tenant_id == user.tenant_id &&
!doc.roles.includes("super_admin")
Pattern: Shared Reference Data
Some collections may be tenant-agnostic:
collections :
countries : # Shared reference data
access :
tenant_field : "" # Empty = no tenant scoping
fields :
code :
type : string
name :
type : string
Pattern: Tenant Quotas
Enforce per-tenant limits:
policies :
products :
user :
actions : [ create ]
when : |
count("products", {tenant_id: user.tenant_id}) < 1000
Best Practices
Always include tenant_id in JWT tokens
Use consistent tenant field names across collections
Test tenant isolation thoroughly
Limit super admin roles to specific users
Monitor cross-tenant access attempts
Use separate databases for high-security tenants
Document tenant-specific features in your API
Troubleshooting
Tenant ID Missing
{
"error" : {
"code" : "forbidden" ,
"message" : "tenant context required"
}
}
Solution: Ensure JWT includes tenant_id claim.
Cross-Tenant Access Denied
{
"error" : {
"code" : "not_found" ,
"message" : "document not found"
}
}
Solution: Document exists but belongs to different tenant. This is expected behavior.
If tenant field is missing from schema, documents won’t be scoped:
collections :
products :
access :
tenant_field : tenant_id # Required!
Next Steps
Field-Level Permissions Control access to specific fields
Hierarchical RBAC Manage user hierarchies and subordinate access