Tenant Filtering
Documentation of how tenant-scoped data isolation is enforced at the ORM level, including intentional bypasses and their justifications.
How Tenant Filtering Worksโ
All entities extending TenantBaseEntity or TenantOrganizationBaseEntity are automatically scoped by tenantId:
// TenantAwareCrudService automatically adds:
// WHERE tenant_id = :currentTenantId
const employees = await this.employeeService.findAll();
// Returns only employees for the authenticated user's tenant
Automatic Filteringโ
Services that extend TenantAwareCrudService automatically inject the current user's tenantId into:
find()/findAll()โ SELECT queriesfindOne()โ single record queriescreate()โ INSERT operations (sets tenantId)update()โ UPDATE operations (scoped by tenantId)delete()โ DELETE operations (scoped by tenantId)
Intentional Bypassesโ
Some scenarios intentionally bypass tenant filtering. Every bypass must be documented and justified.
Category 1: Cross-Tenant by Designโ
These services must query across all tenants as part of their core functionality:
| Service | Justification |
|---|---|
TenantService.findAll() | Super Admin listing all tenants |
UserService.findByEmail() | Email lookup during login (pre-auth) |
AuthService.validateUser() | Authentication (no tenant context yet) |
HealthService.check() | System health check (no tenant context) |
Category 2: Manual QueryBuilder Usageโ
Services using createQueryBuilder() bypass automatic tenant scoping and must manually include tenantId:
// โ
CORRECT โ includes tenant_id filter
const result = await this.repository
.createQueryBuilder("employee")
.where("employee.tenantId = :tenantId", {
tenantId: RequestContext.currentTenantId(),
})
.getMany();
// โ WRONG โ missing tenant_id filter
const result = await this.repository.createQueryBuilder("employee").getMany();
Category 3: Plugin Servicesโ
Integration plugins that sync external data may operate outside standard tenant context:
| Plugin | Justification |
|---|---|
| GitHub Integration | Webhook handlers receive data without user context |
| Upwork Integration | Background sync jobs with stored credentials |
| Job Search | Cross-tenant job matching |
Category 4: Base Class Internalโ
Internal methods in CrudService and TenantAwareCrudService that implement the filtering logic itself:
| Method | Justification |
|---|---|
CrudService.findAll() | Base implementation without tenant scope |
CrudService.findOne() | Overridden by TenantAwareCrudService |
Audit Rulesโ
New Servicesโ
All new services must:
- Extend
TenantAwareCrudService(default) - Or document the bypass with justification
- Never use raw
Repositorywithout tenant filtering
Manual QueryBuilderโ
All createQueryBuilder usages must:
- Include
.where('entity.tenantId = :tenantId', { tenantId })clause - Use
RequestContext.currentTenantId()for the tenant ID - Be audited for tenant isolation compliance
Code Review Checklistโ
- โ
Does the service extend
TenantAwareCrudService? - โ
If using
createQueryBuilder, istenantIdin the WHERE clause? - โ If bypassing tenant filtering, is there a documented justification?
- โ Are plugin services properly scoped when handling multi-tenant data?
- โ Do background jobs use stored tenant context?
Configurationโ
Super Admin Cross-Tenant Accessโ
# Allow Super Admin to query across tenants
ALLOW_SUPER_ADMIN_ROLE=true
Super Admin users can access the TenantController to list and manage all tenants.
Maintenance Guidelinesโ
Adding a New Bypassโ
- Document the service, method, and justification in this page
- Add a code comment explaining the bypass
- Ensure Super Admin or system-level authorization is in place
- Submit for code review with explicit security review
Auditing Existing Bypassesโ
Periodically audit all services for:
- Unauthorized
Repository.find()usage (bypasses tenant filter) - Missing
tenantIdincreateQueryBuilderqueries - New plugin services without documentation
- Deprecated bypasses that can be removed
Related Pagesโ
- Multi-Tenancy โ tenant architecture
- Roles & Permissions โ RBAC
- Database Overview โ general database info