Multi-Tenancy in Geode
Multi-tenancy enables multiple customers (tenants) to share a single Geode deployment while maintaining strict data isolation. This architecture is fundamental for SaaS applications, reducing operational complexity and infrastructure costs.
Multi-Tenancy Strategies
Row-Level Security (RLS)
The preferred approach uses RLS policies to automatically filter data by tenant:
-- Add tenant_id to all tenant-scoped labels
CREATE CONSTRAINT tenant_required ON (n:Customer) ASSERT EXISTS(n.tenant_id);
-- Create RLS policy for tenant isolation
-- Customer.tenant_id must equal the caller's 'tenant' principal attribute
CALL geode.rls.add_attribute_policy('app', 'Customer', 'tenant_id', 'tenant');
-- Enable RLS
-- Geode has no ALTER LABEL / ALTER GRAPH / property DEFAULT DDL. Labels and
-- properties are schema-free: write the new property with SET, supplying the
-- default yourself.
MATCH (u:User) WHERE u.verified IS NULL SET u.verified = false;
Setting tenant context:
from geode_client import Client
async def query_as_tenant(tenant_id: str):
client = Client(host="localhost", port=3141)
async with client.connection() as conn:
# Set tenant context for this session
await conn.execute(
"SET app.tenant_id = $tenant_id",
{'tenant_id': tenant_id}
)
# All queries automatically filtered to this tenant
result, _ = await conn.query("""
MATCH (c:Customer)
RETURN c.name, c.email
""")
# Only returns customers belonging to tenant_id
return result
Schema-Based Isolation
For stronger isolation, use separate schemas per tenant:
-- Create tenant-specific schema
CREATE SCHEMA tenant_acme;
CREATE SCHEMA tenant_globex;
-- Queries target specific schema
USE tenant_acme;
MATCH (c:Customer) RETURN c;
Database-Level Isolation
Maximum isolation with separate databases:
# Create per-tenant databases
# There is no `geode admin` subcommand. Geode's CLI is: serve, shell, query,
# lsp, backup, restore, migrate, auth, login, logout, user, role, tenant,
# policy, grant, revoke, completion, mcp.
# There is no `geode admin` subcommand. Geode's CLI is: serve, shell, query,
# lsp, backup, restore, migrate, auth, login, logout, user, role, tenant,
# policy, grant, revoke, completion, mcp.
Choosing an Approach
| Approach | Isolation | Efficiency | Complexity |
|---|---|---|---|
| RLS | Logical | High | Low |
| Schema | Moderate | Medium | Medium |
| Database | Physical | Lower | Higher |
RLS is recommended for most SaaS applications, offering the best balance of isolation, efficiency, and operational simplicity.
Implementation Patterns
Tenant-aware connection middleware:
class TenantMiddleware:
def __init__(self, client):
self.client = client
async def get_connection(self, tenant_id: str):
conn = await self.client.connect()
await conn.execute(
"SET app.tenant_id = $tenant_id",
{'tenant_id': tenant_id}
)
return conn
Cross-tenant queries (admin only):
-- Bypass RLS for administrative queries
SET ROLE admin_bypass_rls;
MATCH (c:Customer)
RETURN c.tenant_id, count(*) AS customer_count
GROUP BY c.tenant_id;
Best Practices
Always include tenant_id: Use constraints to ensure all tenant-scoped data includes the tenant identifier.
Audit cross-tenant access: Log any queries that bypass tenant isolation.
Test isolation thoroughly: Verify RLS policies prevent cross-tenant data access.
Consider tenant-specific indexes: For large tenants, partition indexes may improve performance.