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โ
| Category | Justification | Example |
|---|---|---|
| Cross-Tenant by Design | Must query across all tenants | TenantService.findAll(), UserService.findByEmail() |
| Manual QueryBuilder | Complex queries using createQueryBuilder | Aggregation queries, reporting |
| Plugin Services | Integration plugins that sync external data | GitHub sync, Upwork import |
| Base Class Internal | Internal ORM operations | CrudService base methods |
Security Rules for Bypassesโ
- Every bypass must be documented in the tenant filtering doc
- Manual
createQueryBuildercalls must includetenantIdin WHERE clauses - Cross-tenant queries require SUPER_ADMIN role guard
- New services must use
TenantAwareCrudServiceunless 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
TenantAwareCrudServicefor all new services - โ
Use
TenantOrganizationBaseEntityfor new entities - โ Document any tenant filtering bypasses
- โ
Use
RequestContext.currentTenantId()in manual queries - โ
Apply
TenantPermissionGuardto all controllers
DON'Tโ
- โ Use
Repository.find()directly (bypasses tenant filtering) - โ Create entities without extending
TenantBaseEntity - โ Allow users to specify
tenantIdin request bodies (use RequestContext) - โ Query across tenants without SUPER_ADMIN authorization
Related Pagesโ
- Tenant Filtering โ detailed bypass documentation
- Roles and Permissions โ role hierarchy
- Registration and Onboarding โ tenant creation flow
- Backend Architecture โ guard architecture