Skip to content

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:

  1. Company-scoped (default) - Document visible to all company members
  2. Cross-company sharing - Explicitly share with other companies (PCAs sharing reports with clients)
  3. User-specific sharing - Share with individual users across any company
  4. 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 ForbiddenException

Implementation 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

  1. Service Layer - CompanyAwareService applies filters to all queries
  2. Validation Layer - UserUtils.validateCompanyAccess() on mutations
  3. Database Layer - Compound indexes ensure query performance
  4. 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 THIS

Frontend 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