Multi-Tenancy
Sibyl supports multiple organizations with complete data isolation. Each organization gets its own knowledge graph, ensuring security and separation.
How It Works
Isolated Graphs
FalkorDB supports multiple named graphs within a single database instance. Sibyl uses the organization UUID as the graph name:
# Each org gets its own graph
graph_name = str(org.id) # e.g., "550e8400-e29b-41d4-a716-446655440000"Organization Context
Every graph operation requires organization context. There are no defaults - callers must explicitly provide org scope:
# EntityManager requires group_id
manager = EntityManager(client, group_id=str(org.id))
# This will raise an error
manager = EntityManager(client, group_id="") # ValueError!Organization Management
Creating Organizations
Organizations are typically created through the web UI or API:
# Via API
curl -X POST http://localhost:3334/api/orgs \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "My Organization"}'Listing Organizations
sibyl org listSwitching Organizations
# Set active organization
sibyl org switch org_abc123
# Check current organization
sibyl org currentAuthentication and Authorization
JWT Tokens
JWT tokens include organization context in the org claim:
# Token payload
{
"sub": "user_abc123", # User ID
"org": "org_xyz789", # Organization ID
"exp": 1234567890 # Expiration
}API Keys
API keys are scoped to an organization:
# Create org-scoped API key
sibyl auth api-key create --name "CI/CD" --scopes mcp,api:readMCP Authentication
MCP requests extract organization from the authenticated token:
# In server.py
async def _require_org_id() -> str:
org_id = await _get_org_id_from_context()
if not org_id:
raise ValueError("Organization context required")
return org_idCode Patterns
EntityManager Pattern
Always create EntityManager with explicit org context:
from sibyl_core.graph import GraphClient, EntityManager
client = await GraphClient.create()
# CORRECT - explicit org context
manager = EntityManager(client, group_id=str(org_id))
# WRONG - no org context
manager = EntityManager(client, group_id="") # Raises ValueErrorQuery Pattern
Queries must include group_id filter:
# CORRECT - scoped to org
result = await driver.execute_query(
"""
MATCH (n)
WHERE n.group_id = $group_id AND n.entity_type = $type
RETURN n
""",
group_id=str(org_id),
type="task"
)
# WRONG - no org scope (queries wrong graph!)
result = await driver.execute_query(
"""
MATCH (n)
WHERE n.entity_type = $type
RETURN n
""",
type="task"
)Driver Cloning
For org-specific operations, clone the driver:
# Clone driver for org-specific graph
org_driver = client.client.driver.clone(str(org_id))
# Now queries go to the org's graph
result = await org_driver.execute_query(query)API Routes
Organization Context in Routes
API routes extract org context from the authenticated user:
from sibyl.auth.context import get_current_org
@router.get("/entities")
async def list_entities():
org = await get_current_org()
manager = EntityManager(client, group_id=str(org.id))
return await manager.list_all()Route Files
Key route files handling organization context:
| File | Purpose |
|---|---|
routes/orgs.py | Organization CRUD |
routes/org_members.py | Membership management |
routes/org_invitations.py | Invitation handling |
routes/entities.py | Entity operations (org-scoped) |
routes/tasks.py | Task operations (org-scoped) |
CLI Organization Support
Switching Context
# Switch active organization
sibyl org switch org_abc123
# Commands now use this org context
sibyl task list # Lists tasks in org_abc123Per-Command Override
# Override context for single command
sibyl --context org_xyz task list
# Or
SIBYL_CONTEXT=org_xyz sibyl task listDatabase Schema
Organization Table
-- In PostgreSQL
CREATE TABLE organizations (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Users belong to organizations
CREATE TABLE organization_members (
organization_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES users(id),
role VARCHAR(50) DEFAULT 'member',
PRIMARY KEY (organization_id, user_id)
);Graph Storage
FalkorDB Instance
├── Graph: "550e8400-e29b-41d4-a716-446655440000" (Org A)
│ ├── Entity nodes
│ ├── Episodic nodes
│ └── Relationships
├── Graph: "6fa459ea-ee8a-3ca4-894e-db77e160355e" (Org B)
│ └── (completely isolated)
└── Graph: "default" (system/anonymous)Security Considerations
Data Isolation
Organizations are completely isolated:
- No cross-org queries - Queries only see their org's graph
- No shared data - Each org has its own nodes and relationships
- No data leakage - Embeddings are org-scoped
Access Control
# Verify user belongs to requested org
async def require_org_access(user: User, org_id: str) -> Organization:
org = await get_org(org_id)
if not await user.has_access_to(org):
raise PermissionDenied("No access to organization")
return orgAudit Logging
Track organization access:
log.info(
"org_access",
user_id=user.id,
org_id=org.id,
action="list_entities"
)Common Issues
Wrong Organization Data
Symptom: Seeing data from another organization or no data at all.
Cause: Missing or incorrect group_id in queries.
Fix: Verify org context is being passed correctly:
# Debug: log the org_id being used
log.debug("query_context", org_id=org_id)"Graph not found"
Symptom: Queries fail with graph not found errors.
Cause: New organization with no entities yet.
Fix: The graph is created automatically when first entity is added.
Permission Denied
Symptom: User can't access organization resources.
Fix: Verify user is a member of the organization:
sibyl org list # Shows orgs user belongs toBest Practices
1. Always Validate Org Context
if not org_id:
raise ValueError("Organization context required")2. Use Type Hints
async def my_function(*, group_id: str) -> None:
"""Forces callers to use keyword argument."""
...3. Log Organization Context
log.info("operation", org_id=org_id, action="create_entity")4. Test Multi-Tenancy
async def test_org_isolation():
# Create entities in org A
await create_entity(org_id=org_a)
# Query from org B should not see them
results = await search(org_id=org_b)
assert len(results) == 0Next Steps
- MCP Configuration - Configure organization-scoped MCP access
- Knowledge Graph - Understand graph structure
- Task Management - Org-scoped task operations
