ื“ืœื’ ืœืชื•ื›ืŸ ื”ืจืืฉื™

Multi-Tenancy

Ever Gauzy is built as a multi-tenant platform where every piece of data is scoped to a specific tenant. This ensures complete data isolation between different organizations using the platform.

Tenant Modelโ€‹

What is a Tenant?โ€‹

A tenant represents a top-level organizational boundary โ€” typically a company or business entity that uses the Gauzy platform. Each tenant has:

  • Its own users, employees, and organizations
  • Isolated data (invoices, time logs, expenses, etc.)
  • Independent configuration and settings
  • Separate roles and permissions

Tenant Hierarchyโ€‹

Tenant (Company)
โ”œโ”€โ”€ Organization A (Branch / Division)
โ”‚ โ”œโ”€โ”€ Department 1
โ”‚ โ”‚ โ”œโ”€โ”€ Team Alpha
โ”‚ โ”‚ โ””โ”€โ”€ Team Beta
โ”‚ โ”œโ”€โ”€ Department 2
โ”‚ โ””โ”€โ”€ Projects
โ”‚ โ”œโ”€โ”€ Project X
โ”‚ โ””โ”€โ”€ Project Y
โ”œโ”€โ”€ Organization B
โ”‚ โ””โ”€โ”€ ...
โ””โ”€โ”€ Users
โ”œโ”€โ”€ Super Admin (tenant-wide)
โ”œโ”€โ”€ Admin (per organization)
โ””โ”€โ”€ Employee (per organization)

How Tenant Isolation Worksโ€‹

Entity Base Classesโ€‹

All tenant-scoped entities extend TenantBaseEntity or TenantOrganizationBaseEntity:

// Tenant-scoped entity
export abstract class TenantBaseEntity extends BaseEntity {
@MultiORMManyToOne(() => Tenant, { nullable: false })
tenant: ITenant;

@MultiORMColumn({ relationId: true })
tenantId: string;
}

// Tenant + Organization scoped entity
export abstract class TenantOrganizationBaseEntity extends TenantBaseEntity {
@MultiORMManyToOne(() => Organization, { nullable: true })
organization?: IOrganization;

@MultiORMColumn({ relationId: true, nullable: true })
organizationId?: string;
}

Automatic Tenant Filteringโ€‹

The TenantAwareCrudService base class automatically injects the current tenant's ID into all queries:

// In TenantAwareCrudService
async findAll(options?: FindManyOptions<T>): Promise<IPagination<T>> {
const tenantId = RequestContext.currentTenantId();

// Automatically adds WHERE tenant_id = :tenantId
return super.findAll({
...options,
where: {
...options?.where,
tenantId,
} as any,
});
}

This means:

  • SELECT queries only return data for the current tenant
  • INSERT operations automatically set tenantId
  • UPDATE and DELETE operations are scoped to the current tenant
  • No tenant data leaks are possible through the service layer

Request Contextโ€‹

The tenant is resolved from the JWT token on every request:

JWT Token โ†’ Auth Guard โ†’ Tenant Resolve โ†’ RequestContext.currentTenantId()

The RequestContext makes the tenant ID available throughout the request lifecycle:

const tenantId = RequestContext.currentTenantId();

Tenant Lifecycleโ€‹

1. Tenant Creationโ€‹

When a user registers publicly, a new tenant is created during onboarding:

// TenantService.onboardTenant()
async onboardTenant(user: IUser, input: TenantInput): Promise<ITenant> {
// Create tenant
const tenant = await this.tenantRepository.save({
name: input.name,
id: uuidv4(),
});

// Create default roles for the tenant
await this.roleService.createBulk(tenant.id);

// Assign SUPER_ADMIN role to the creating user
await this.userRepository.update(user.id, {
tenantId: tenant.id,
roleId: await this.findSuperAdminRole(tenant.id),
});

return tenant;
}

2. Organization Creationโ€‹

Within a tenant, multiple organizations can be created:

const organization = await this.organizationService.create({
name: "Engineering Division",
tenantId: RequestContext.currentTenantId(),
currency: "USD",
defaultValueDateType: "TODAY",
});

3. User-Tenant Associationโ€‹

Users are always associated with exactly one tenant:

User (id, email, tenantId, roleId)
โ””โ”€โ”€ tenantId references Tenant(id)

Tenant Filtering Bypassesโ€‹

Some scenarios intentionally bypass tenant filtering. These are documented in detail in the Tenant Filtering page.

Categories of Bypassโ€‹

CategoryJustificationExample
Cross-Tenant by DesignMust query across all tenantsTenantService.findAll(), UserService.findByEmail()
Manual QueryBuilderComplex queries using createQueryBuilderAggregation queries, reporting
Plugin ServicesIntegration plugins that sync external dataGitHub sync, Upwork import
Base Class InternalInternal ORM operationsCrudService base methods

Security Rules for Bypassesโ€‹

  1. Every bypass must be documented in the tenant filtering doc
  2. Manual createQueryBuilder calls must include tenantId in WHERE clauses
  3. Cross-tenant queries require SUPER_ADMIN role guard
  4. New services must use TenantAwareCrudService unless explicitly justified

Multi-Tenant Guardsโ€‹

TenantPermissionGuardโ€‹

The primary guard that enforces tenant isolation at the API level:

@UseGuards(TenantPermissionGuard)
@Controller("employee")
export class EmployeeController {
// All endpoints automatically scoped to the user's tenant
}

OrganizationPermissionGuardโ€‹

For endpoints that require organization-level access:

@UseGuards(OrganizationPermissionGuard)
@Controller("project")
export class ProjectController {
// Endpoints scoped to user's organization within their tenant
}

Data Flow Exampleโ€‹

Configurationโ€‹

Allow Super Admin Roleโ€‹

# .env
ALLOW_SUPER_ADMIN_ROLE=true # Allow creating Super Admin users

When disabled, no new SUPER_ADMIN users can be created, enhancing security for production multi-tenant deployments.

Best Practicesโ€‹

DOโ€‹

  • โœ… Extend TenantAwareCrudService for all new services
  • โœ… Use TenantOrganizationBaseEntity for new entities
  • โœ… Document any tenant filtering bypasses
  • โœ… Use RequestContext.currentTenantId() in manual queries
  • โœ… Apply TenantPermissionGuard to all controllers

DON'Tโ€‹

  • โŒ Use Repository.find() directly (bypasses tenant filtering)
  • โŒ Create entities without extending TenantBaseEntity
  • โŒ Allow users to specify tenantId in request bodies (use RequestContext)
  • โŒ Query across tenants without SUPER_ADMIN authorization