Multi-Tenancy Architecture
Our application implements a robust multi-tenancy model built around Companies as the primary organizational boundary. Every data entity belongs to a company and follows consistent access control patterns through the CompanyAware system.
Core Concepts
Company (Organization)
Companies are the primary tenancy boundaries in the system. In agricultural contexts, companies represent:
- Farms - Individual farming operations
- Advisors/PCAs - Agricultural consulting firms
- Cooperatives - Farming cooperatives
- Suppliers - Input suppliers, equipment dealers
- Processors - Food processing facilities
Users and Memberships
Users are cross-organizational actors who can belong to multiple companies through Memberships:
typescript
interface User {
_id: ObjectId;
profile: { firstName: string; lastName: string };
memberships: Membership[]; // User can belong to multiple companies
identities: Identity[]; // Multiple auth methods
}
interface Membership {
companyId: ObjectId; // Reference to Company
roles: string[]; // ['owner', 'manager', 'operator', 'viewer']
attrs: Record<string, any>; // Custom company-specific attributes
}CompanyAware Documents
All business data entities (reports, attachments, messages, etc.) inherit from CompanyAwareBase and include:
typescript
interface CompanyAwareDocument {
companyId: ObjectId; // Primary company owner
visibility: 'private' | 'company' | 'public';
sharedWithCompanies: ObjectId[]; // Cross-company sharing
sharedWithUsers: ObjectId[]; // Individual user sharing
sharedWithChatRooms: ObjectId[]; // Room-based sharing
}Architecture Diagram
Access Control Model
Multi-Level Access
The system provides three levels of sharing:
- Company-scoped (default) - Document visible to all company members
- Cross-company sharing - Explicitly share with other companies (PCAs sharing reports with clients)
- User-specific sharing - Share with individual users across any company
- Public - Visible to all authenticated users
Access Query Pattern
All queries automatically filter based on user's company memberships:
typescript
// Built by CompanyAwareService.applyCompanyFilter()
{
$or: [
{ companyId: { $in: userCompanyIds } }, // User's companies
{ sharedWithCompanies: { $in: userCompanyIds } }, // Shared with user's companies
{ sharedWithUsers: { $in: [userId] } }, // Directly shared with user
{ visibility: 'public' }, // Public documents
];
}UserUtils Helper
Centralized utilities for company access validation:
typescript
// Get all company IDs user belongs to
UserUtils.membershipObjectIds(user): Types.ObjectId[]
// Validate user has access to specific company
UserUtils.validateCompanyAccess(user, companyId): void // throws ForbiddenExceptionImplementation Components
1. Base Schema (Mongoose)
typescript
// src/common/schemas/common.model.ts
export abstract class CompanyAwareBase {
@Prop({
type: Schema.Types.ObjectId,
ref: 'Company',
required: true,
index: true,
})
companyId: Types.ObjectId;
@Prop({
type: String,
enum: ['private', 'company', 'public'],
default: 'private',
})
visibility: 'private' | 'company' | 'public';
@Prop([{ type: Schema.Types.ObjectId, ref: 'Company' }])
sharedWithCompanies: Types.ObjectId[];
@Prop([{ type: Schema.Types.ObjectId, ref: 'User' }])
sharedWithUsers: Types.ObjectId[];
@Prop([{ type: Schema.Types.ObjectId, ref: 'ChatRoom' }])
sharedWithChatRooms: Types.ObjectId[];
}2. Zod Validation Schema
typescript
// src/common/schemas/common.zod.ts
export const CompanyAwareSchema = z.object({
companyId: MongoObjectIdSchema,
visibility: z.enum(['private', 'company', 'public']).default('private'),
sharedWithCompanies: z.array(MongoObjectIdSchema).default([]),
sharedWithUsers: z.array(MongoObjectIdSchema).default([]),
sharedWithRooms: z.array(MongoObjectIdSchema).default([]),
});3. Base Service
typescript
export abstract class CompanyAwareService<T extends CompanyAwareDocument> {
constructor(protected readonly model: Model<T>) {}
// Automatic company filtering on all queries
protected applyCompanyFilter(
filter: FilterQuery<T>,
user: UserDocumentPopulated,
): FilterQuery<T>;
// CRUD operations with automatic access control
async findAll(user: UserDocumentPopulated): Promise<T[]>;
async findByIdWithAccess(id: string, user: UserDocumentPopulated): Promise<T>;
async create(createDto: any, user: UserDocumentPopulated): Promise<T>;
async update(
id: string,
updateDto: any,
user: UserDocumentPopulated,
): Promise<T>;
async remove(id: string, user: UserDocumentPopulated): Promise<T>;
async findByCompany(
companyId: string,
user: UserDocumentPopulated,
): Promise<T[]>;
}4. Schema Helper
typescript
// Add CompanyAware features to any schema
export function addCompanyAwareFeatures(schema: Schema) {
schema.add(CompanyAwareSchemaFields);
Object.assign(schema.statics, CompanyAwareStatics);
Object.assign(schema.methods, CompanyAwareInstanceMethods);
// Performance indexes
schema.index({ companyId: 1, visibility: 1 });
schema.index({ sharedWithCompanies: 1 });
schema.index({ sharedWithUsers: 1 });
}Use Cases
1. Grower (Farm Owner)
- Creates company for their farm
- Invites family members and employees as company members
- All farm data (fields, reports, schedules) belongs to farm company
- Can share specific reports with their PCA advisor
2. PCA (Agricultural Advisor)
- Creates advisory company
- Has memberships in 50+ client farm companies
- Creates scouting reports that belong to client companies
- Can share reports across companies (e.g., regional insights)
3. Worker
- Member of multiple farm companies (seasonal work)
- Frontend CompanySelector component to choose active company
- All chat messages and task reports tagged with selected company
- No data leakage between companies
4. Cooperative
- Central company with multiple member farms
- Can access aggregated data from member farms (with sharing)
- Provides services and resources to members
- Cross-company collaboration on projects
Data Isolation Guarantees
Enforcement Points
- Service Layer - CompanyAwareService applies filters to all queries
- Validation Layer - UserUtils.validateCompanyAccess() on mutations
- Database Layer - Compound indexes ensure query performance
- Frontend Layer - CompanyContext manages selected company state
Security Patterns
typescript
// ✅ CORRECT: Service automatically filters by user's companies
await reportService.findAll(user);
// ✅ CORRECT: Validates user has access before update
await reportService.update(reportId, updateDto, user);
// ❌ WRONG: Direct model access bypasses security
await this.reportModel.find({}).exec(); // DON'T DO THISFrontend Integration
CompanyContext (React)
typescript
const {
selectedCompanyId,
selectedMembership,
setSelectedCompany,
isManager,
hasRole,
} = useCompany();
// All API calls include companyId
fetchCompanyParcels(selectedCompanyId);
getVoiceConfig(workerPhone, selectedCompanyId);Company Selection
- Users with multiple memberships see CompanySelector component
- Selected company persists in context state
- All subsequent operations scoped to selected company
- Explicit company selection required for voice/call-based reporting
Performance Considerations
Indexing Strategy
All CompanyAware schemas include:
typescript
schema.index({ companyId: 1, visibility: 1 });
schema.index({ sharedWithCompanies: 1 });
schema.index({ sharedWithUsers: 1 });Query Optimization
- Company filter always applied first (indexed)
- Compound indexes for common query patterns
- Population hints for referenced companies
Benefits
Security
- ✅ Zero-trust access model - explicit company membership required
- ✅ Automatic query filtering prevents data leakage
- ✅ Centralized validation through UserUtils
- ✅ Audit trail on all company-scoped operations
Flexibility
- ✅ Multi-company users (advisors, consultants, cooperatives)
- ✅ Cross-company collaboration through sharing
- ✅ Granular sharing (company, user, or public)
- ✅ Role-based permissions within companies
Scalability
- ✅ Natural sharding boundary (companyId)
- ✅ Independent company growth
- ✅ Query performance through strategic indexing
- ✅ Resource isolation and monitoring per company