Skip to main content

Multi-Tenancy Architecture

The platform implements a robust multi-tenant architecture ensuring complete data isolation between workspaces. This is critical for security when multiple users or organizations share the same platform instance.

Three-Layer Protection

┌─────────────────────────────────────────────────────────────────────────┐
│ Request Flow │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ Layer 1: Middleware Authorization │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ authenticate → loadWorkspace → requireWorkspaceAccess │ │
│ │ │ │
│ │ • JWT validation │ │
│ │ • Workspace membership check │ │
│ │ • Permission verification (canQuery, canInvite, etc.) │ │
│ │ • BOLA protection │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ Layer 2: Database-Level Isolation │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ AsyncLocalStorage Tenant Context │ │
│ │ │ │
│ │ • Request-scoped context │ │
│ │ • Automatic query filtering │ │
│ │ • Cross-tenant access detection │ │
│ │ • Mongoose plugin integration │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ Layer 3: Vector Store Isolation │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Mandatory Qdrant Filter │ │
│ │ │ │
│ │ • workspaceId REQUIRED for all queries │ │
│ │ • Throws error if missing │ │
│ │ • All chunks tagged with workspaceId │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘

Layer 1: Middleware Authorization

Authentication Middleware

// middleware/auth.js

export const authenticate = async (req, res, next) => {
try {
const token = req.cookies.accessToken ||
req.headers.authorization?.replace('Bearer ', '');

if (!token) {
throw new AppError('Authentication required', 401);
}

const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);

if (!user) {
throw new AppError('User not found', 401);
}

req.user = user;
next();
} catch (error) {
next(new AppError('Invalid token', 401));
}
};

Workspace Loading with BOLA Protection

// middleware/loadWorkspace.js

export const loadWorkspace = async (req, res, next) => {
try {
const workspaceId = req.headers['x-workspace-id'] ||
req.body?.workspaceId ||
req.query?.workspaceId;

if (!workspaceId) {
throw new AppError('Workspace ID required', 400);
}

const workspace = await Workspace.findById(workspaceId);

if (!workspace) {
throw new AppError('Workspace not found', 404);
}

// BOLA Protection: Verify user access
const isMember = workspace.members?.some(
m => m.user?.toString() === req.user._id.toString()
);
const isOwner = workspace.owner?.toString() === req.user._id.toString();

if (!isMember && !isOwner) {
logger.warn('BOLA attempt detected', {
userId: req.user._id,
workspaceId,
action: 'access_denied',
});
throw new AppError('Access denied to this workspace', 403);
}

req.workspace = workspace;
next();
} catch (error) {
next(error);
}
};

Permission Verification

// middleware/workspaceAuth.js

export const requireWorkspaceAccess = (permission = 'canQuery') => {
return async (req, res, next) => {
const workspace = req.workspace;
const userId = req.user._id.toString();

// Owner has all permissions
if (workspace.owner?.toString() === userId) {
return next();
}

// Check member permissions
const member = workspace.members?.find(
m => m.user?.toString() === userId
);

if (!member) {
throw new AppError('Not a workspace member', 403);
}

if (!member.permissions?.[permission]) {
throw new AppError(`Permission denied: ${permission}`, 403);
}

next();
};
};

export const requireWorkspaceOwner = async (req, res, next) => {
const workspace = req.workspace;
const userId = req.user._id.toString();

if (workspace.owner?.toString() !== userId) {
throw new AppError('Owner access required', 403);
}

next();
};

Layer 2: Database-Level Isolation

AsyncLocalStorage Context

// services/tenantIsolation.js

import { AsyncLocalStorage } from 'async_hooks';

const tenantContext = new AsyncLocalStorage();

export function withTenantContext(context, fn) {
return tenantContext.run(context, fn);
}

export function getCurrentTenant() {
return tenantContext.getStore();
}

Mongoose Plugin

// services/tenantIsolation.js

export function tenantIsolationPlugin(schema) {
// Auto-filter queries by workspaceId
schema.pre(['find', 'findOne', 'countDocuments', 'aggregate'], function() {
const ctx = getCurrentTenant();

if (ctx?.workspaceId && !this._skipTenantFilter) {
this.where({ workspaceId: ctx.workspaceId });
}
});

// Detect cross-tenant access attempts
schema.post(['find', 'findOne'], function(docs) {
const ctx = getCurrentTenant();

if (!ctx?.workspaceId) return;

const results = Array.isArray(docs) ? docs : [docs].filter(Boolean);

for (const doc of results) {
if (doc.workspaceId && doc.workspaceId.toString() !== ctx.workspaceId) {
logger.error('Cross-tenant access detected!', {
requestedWorkspace: ctx.workspaceId,
documentWorkspace: doc.workspaceId,
documentId: doc._id,
});
throw new Error('Cross-tenant access violation');
}
}
});
}

Usage in Controllers

// controllers/ragController.js

export const askQuestion = catchAsync(async (req, res) => {
const { question } = req.body;
const workspaceId = req.workspace._id;
const userId = req.user._id;

// Run entire operation within tenant context
const result = await withTenantContext(
{ workspaceId: workspaceId.toString(), userId: userId.toString() },
async () => {
return ragService.askWithConversation(question, {
conversationId,
workspaceId,
});
}
);

sendSuccess(res, result);
});

Layer 3: Vector Store Isolation

Mandatory Workspace Filter

// services/rag/queryRetrieval.js

export function buildQdrantFilter(filters, workspaceId) {
// CRITICAL: workspaceId is always required
if (!workspaceId || typeof workspaceId !== 'string') {
throw new Error(
'workspaceId is required for vector store queries (multi-tenant isolation)'
);
}

const qdrantFilter = { must: [] };

// ALWAYS add workspace filter first
qdrantFilter.must.push({
key: 'metadata.workspaceId',
match: { value: workspaceId },
});

// Additional optional filters
if (filters?.page) {
qdrantFilter.must.push({
key: 'metadata.sourceId',
match: { value: filters.page },
});
}

if (filters?.section) {
qdrantFilter.must.push({
key: 'metadata.section',
match: { value: filters.section },
});
}

return qdrantFilter;
}

Permission Model

Workspace Roles

RoleDescriptionPermissions
ownerWorkspace creatorAll permissions
adminAdministratorManage members, sync
memberRegular memberQuery, view sources
viewerRead-only accessQuery only

Permission Flags

const MemberPermissions = {
canQuery: Boolean, // Ask questions
canViewSources: Boolean, // View source documents
canInvite: Boolean, // Invite new members
canManageSync: Boolean, // Trigger syncs
canEditSettings: Boolean, // Modify workspace settings
};

Role-Permission Mapping

const ROLE_PERMISSIONS = {
owner: {
canQuery: true,
canViewSources: true,
canInvite: true,
canManageSync: true,
canEditSettings: true,
},
admin: {
canQuery: true,
canViewSources: true,
canInvite: true,
canManageSync: true,
canEditSettings: false,
},
member: {
canQuery: true,
canViewSources: true,
canInvite: false,
canManageSync: false,
canEditSettings: false,
},
viewer: {
canQuery: true,
canViewSources: false,
canInvite: false,
canManageSync: false,
canEditSettings: false,
},
};

Security Properties

PropertyImplementation
No Cross-Tenant ReadsQdrant filter + Mongoose plugin
No Cross-Tenant WritesMiddleware + tenant context
BOLA PreventionExplicit membership verification
Privilege Escalation PreventionRole-based permissions
Audit TrailAll access logged with context

Testing Isolation

// Test that cross-tenant access is blocked
describe('Multi-Tenancy', () => {
it('should block access to other workspace documents', async () => {
const user1 = await createUser();
const user2 = await createUser();
const workspace1 = await createWorkspace(user1);
const workspace2 = await createWorkspace(user2);

// Index document in workspace1
await indexDocument(workspace1._id, 'Secret document');

// Try to query from workspace2
const result = await ragService.askWithConversation(
'Show me the secret document',
{ workspaceId: workspace2._id, userId: user2._id }
);

// Should not find workspace1's document
expect(result.sources).toHaveLength(0);
});

it('should throw on missing workspaceId', async () => {
await expect(
buildQdrantFilter({}, null)
).rejects.toThrow('workspaceId is required');
});
});