Auto-Populating tenantId with @smrt({ tenantScoped: true })
Status: ✅ Feature already implemented (Issue #688, #809)
The @smrt({ tenantScoped: true }) decorator automatically populates tenantId from the current tenant context when you save objects.
Quick Start
1. Define Your SMRT Class
import { SmrtObject, smrt } from '@happyvertical/smrt-core';
@smrt({ tenantScoped: true })
class Build extends SmrtObject {
contractId: string = '';
status: string = '';
// tenantId field is auto-injected by the decorator
}
2. Enable Tenancy at Application Startup
import { enableTenancy } from '@happyvertical/smrt-tenancy';
// Call once at app startup
enableTenancy();
3. Use Within Tenant Context
import { withTenant } from '@happyvertical/smrt-tenancy';
const tenantId = 'tenant-123';
await withTenant({ tenantId }, async () => {
// Create without manually providing tenantId
const build = await buildCollection.create({
contractId: 'contract-abc',
status: 'pending',
// NO tenantId needed! Auto-populated from context
});
console.log(build.tenantId); // 'tenant-123'
});
How It Works
- Decorator:
@smrt({ tenantScoped: true })registers the class with tenancy config - Interceptor:
enableTenancy()registers a global interceptor - Context:
withTenant()establishes AsyncLocalStorage-based context - Auto-populate:
beforeSaveinterceptor injectstenantIdfrom context
Configuration Options
@smrt({
tenantScoped: {
mode: 'required', // 'required' | 'optional'
field: 'tenantId', // Custom field name
autoFilter: true, // Auto-filter queries by tenant
autoPopulate: true, // Auto-populate tenantId on save
allowSuperAdminBypass: false, // Allow cross-tenant operations
}
})
class MyClass extends SmrtObject {
// ...
}
Common Issues
Issue: tenantId Not Auto-Populated
Symptom: You have to manually provide tenantId in factories
Causes:
- ❌
enableTenancy()not called at app startup - ❌ Code not running within
withTenant()context - ❌
@happyvertical/smrt-tenancypackage not installed
Solution:
// ✅ CORRECT
import { enableTenancy, withTenant } from '@happyvertical/smrt-tenancy';
enableTenancy(); // At startup
await withTenant({ tenantId: TEST_TENANT_ID }, async () => {
const build = await buildCollection.create({
contractId: 'abc',
// tenantId auto-populated!
});
});
// ❌ WRONG - No tenant context
const build = await buildCollection.create({
contractId: 'abc',
tenantId: TEST_TENANT_ID, // Manual - not needed!
});
Issue: Tenant Isolation Violation
Symptom: Error when explicitly providing different tenantId
Cause: You're trying to create a record for a different tenant
Solution: Don't override tenantId - let it auto-populate
await withTenant({ tenantId: 'tenant-123' }, async () => {
// ❌ WRONG - Throws TenantIsolationError
const build = await buildCollection.create({
tenantId: 'different-tenant', // Mismatch!
});
// ✅ CORRECT - Auto-populated
const build = await buildCollection.create({
contractId: 'abc',
// tenantId is 'tenant-123'
});
});
Testing
For tests, use withTenant() to establish context:
import { withTenant } from '@happyvertical/smrt-tenancy';
describe('Build', () => {
it('should auto-populate tenantId', async () => {
await withTenant({ tenantId: 'test-tenant' }, async () => {
const build = await buildCollection.create({
contractId: 'abc',
});
expect(build.tenantId).toBe('test-tenant');
});
});
});
Middleware Integration
Express
import { enterTenantContext } from '@happyvertical/smrt-tenancy';
// ⚠️ IMPORTANT: tenantId must come from authenticated user state,
// NOT from client-controlled headers or cookies!
app.use((req, res, next) => {
// Assumes an upstream authentication middleware has populated req.user
// from a verified token or session, and that req.user.tenantId is trusted
// server-side state.
const user = (req as any).user;
const tenantId = user?.tenantId as string | undefined;
if (tenantId) {
enterTenantContext({ tenantId });
}
next();
});
SvelteKit
import { enterTenantContext } from '@happyvertical/smrt-tenancy';
// ⚠️ IMPORTANT: tenantId must come from authenticated session data,
// NOT from client-controlled cookies!
export const handle = async ({ event, resolve }) => {
// Assumes an upstream authentication hook has populated event.locals.user
// from a verified session or token, and that user.tenantId is trusted
// server-side state.
const user = event.locals.user;
const tenantId = user?.tenantId as string | undefined;
if (tenantId) {
enterTenantContext({ tenantId });
}
return resolve(event);
};
Related Documentation
- Issue #688: tenantScoped decorator
- Issue #809: Auto-populate tenantId
- RFC-001: Multi-Tenancy
- Tenancy Package README
FAQ
Q: Do I need to install @happyvertical/smrt-tenancy?
A: Yes! The auto-population happens in the tenancy package interceptor.
Q: Can I use @smrt({ tenantScoped: true }) without the tenancy package?
A: Yes, but you won't get auto-population or auto-filtering. You'll have to manually manage tenantId.
Q: How do I disable auto-population?
A: Set autoPopulate: false in the decorator config:
@smrt({
tenantScoped: {
autoPopulate: false // Manual tenantId required
}
})
Q: Can I use a different field name?
A: Yes, use the field option:
@smrt({
tenantScoped: {
field: 'organizationId' // Use organizationId instead of tenantId
}
})