Tenant Context
Understanding tenant context is crucial for multi-tenant applications using Azotte. This guide explains how to properly manage and utilize tenant information across your integration.
Tenant Architecture
Multi-Tenant Design
Azotte supports a multi-tenant architecture where each tenant represents an independent business entity with isolated data and configurations.
interface Tenant {
id: string;
name: string;
subdomain: string;
created_at: Date;
status: 'active' | 'suspended' | 'pending';
configuration: TenantConfig;
}
interface TenantConfig {
currency: string;
timezone: string;
language: string;
branding: BrandingConfig;
features: FeatureFlags;
}
Tenant Identification
Azotte identifies tenants through multiple methods:
- Subdomain:
tenant-name.azotte.com - Custom Domain:
billing.yourcompany.com - API Key: Tenant-scoped API keys
- Header:
X-Tenant-IDHTTP header
API Context
Authentication Headers
POST /api/subscriptions
Authorization: Bearer sk_live_123abc...
X-Tenant-ID: tenant_456def
Content-Type: application/json
SDK Configuration
import { Azotte } from '@azotte/sdk';
const azotte = new Azotte({
apiKey: 'sk_live_123abc...',
tenantId: 'tenant_456def',
environment: 'production'
});
// All subsequent calls will use this tenant context
const subscription = await azotte.subscriptions.create({
customer_id: 'cus_789ghi',
plan_id: 'plan_basic'
});
Tenant Isolation
Data Segregation
- Database Level: Separate schemas per tenant
- API Level: Tenant-scoped endpoints
- Storage Level: Isolated file systems
- Cache Level: Tenant-specific cache keys
Security Boundaries
interface TenantSecurity {
data_encryption: {
at_rest: boolean;
in_transit: boolean;
key_management: 'tenant_managed' | 'azotte_managed';
};
access_control: {
rbac_enabled: boolean;
sso_integration: boolean;
ip_restrictions: string[];
};
compliance: {
gdpr_compliant: boolean;
pci_dss: boolean;
soc2: boolean;
};
}
Context Management
Request Context
// Express.js middleware example
const tenantContext = (req, res, next) => {
const tenantId = req.headers['x-tenant-id'] ||
req.subdomain ||
extractFromApiKey(req.headers.authorization);
req.tenant = await loadTenantConfig(tenantId);
next();
};
// Usage in routes
app.use('/api', tenantContext);
app.post('/api/customers', (req, res) => {
const tenant = req.tenant;
// Customer creation scoped to tenant
});
Context Switching
class TenantContextManager {
private currentTenant: string | null = null;
switchTenant(tenantId: string): void {
this.currentTenant = tenantId;
this.updateApiContext();
}
getCurrentTenant(): string | null {
return this.currentTenant;
}
private updateApiContext(): void {
// Update SDK configuration
azotte.setTenantContext(this.currentTenant);
}
}
Configuration Management
Per-Tenant Settings
{
"tenant_id": "tenant_456def",
"configuration": {
"billing": {
"currency": "USD",
"tax_calculation": "inclusive",
"invoice_numbering": "sequential"
},
"features": {
"advanced_analytics": true,
"custom_branding": true,
"api_access": true
},
"limits": {
"max_customers": 10000,
"max_subscriptions": 50000,
"api_rate_limit": 1000
}
}
}
Environment-Specific Context
const getTenantConfig = (environment) => {
const configs = {
development: {
tenantId: 'tenant_dev_123',
apiKey: 'sk_test_...',
webhookUrl: 'http://localhost:3000/webhooks'
},
staging: {
tenantId: 'tenant_staging_456',
apiKey: 'sk_test_...',
webhookUrl: 'https://staging.app.com/webhooks'
},
production: {
tenantId: 'tenant_prod_789',
apiKey: 'sk_live_...',
webhookUrl: 'https://app.com/webhooks'
}
};
return configs[environment];
};
Multi-Tenant Webhooks
Tenant-Specific Endpoints
// Register tenant-specific webhook endpoints
const registerWebhooks = async (tenantId) => {
await azotte.webhooks.create({
url: `https://app.com/webhooks/${tenantId}`,
events: ['subscription.*', 'payment.*'],
tenant_id: tenantId
});
};
Webhook Routing
app.post('/webhooks/:tenantId', (req, res) => {
const tenantId = req.params.tenantId;
const event = req.body;
// Validate webhook signature for this tenant
const isValid = azotte.webhooks.validateSignature(
event,
req.headers['azotte-signature'],
tenantId
);
if (isValid) {
processWebhookEvent(event, tenantId);
res.status(200).send('OK');
} else {
res.status(400).send('Invalid signature');
}
});
Tenant Provisioning
Automatic Provisioning
interface TenantProvisioning {
create_tenant: (config: TenantCreateRequest) => Promise<Tenant>;
setup_billing: (tenantId: string) => Promise<void>;
configure_features: (tenantId: string, features: FeatureFlags) => Promise<void>;
setup_webhooks: (tenantId: string, endpoints: string[]) => Promise<void>;
}
// Example provisioning flow
const provisionNewTenant = async (companyInfo: CompanyInfo) => {
const tenant = await azotte.tenants.create({
name: companyInfo.name,
subdomain: companyInfo.subdomain,
configuration: {
currency: companyInfo.currency,
timezone: companyInfo.timezone
}
});
// Setup default billing configuration
await azotte.billing.setupDefaults(tenant.id);
// Configure webhooks
await azotte.webhooks.create({
tenant_id: tenant.id,
url: `https://${companyInfo.subdomain}.app.com/webhooks`,
events: ['*']
});
return tenant;
};
Testing with Tenant Context
Test Isolation
describe('Subscription Management', () => {
const testTenantId = 'tenant_test_123';
beforeEach(async () => {
// Setup test tenant context
azotte.setTenantContext(testTenantId);
// Clean up test data
await cleanupTenantData(testTenantId);
});
it('should create subscription for tenant', async () => {
const subscription = await azotte.subscriptions.create({
customer_id: 'test_customer',
plan_id: 'test_plan'
});
expect(subscription.tenant_id).toBe(testTenantId);
});
});
Mock Tenant Context
const mockTenantContext = {
tenant_id: 'tenant_mock_123',
configuration: {
currency: 'USD',
features: {
advanced_analytics: true
}
}
};
// Mock SDK calls
jest.mock('@azotte/sdk', () => ({
Azotte: jest.fn().mockImplementation(() => ({
getTenantContext: () => mockTenantContext,
subscriptions: {
create: jest.fn().mockResolvedValue(mockSubscription)
}
}))
}));
Best Practices
Context Validation
- Always validate tenant ID before processing requests
- Implement tenant existence checks
- Use tenant-scoped database queries
- Log tenant context for debugging
Error Handling
const validateTenantContext = (tenantId) => {
if (!tenantId) {
throw new Error('Tenant context is required');
}
if (!isValidTenantId(tenantId)) {
throw new Error('Invalid tenant ID format');
}
if (!tenantExists(tenantId)) {
throw new Error('Tenant not found');
}
};
Performance Optimization
- Cache tenant configurations
- Use tenant-scoped connection pools
- Implement tenant-aware caching strategies
- Monitor per-tenant performance metrics
Troubleshooting
Common Issues
- Missing Tenant Context: API calls without proper tenant identification
- Cross-Tenant Data Leakage: Incorrect tenant scoping in queries
- Authentication Errors: Invalid tenant-scoped API keys
- Configuration Conflicts: Mismatched tenant settings
Debugging Tips
// Enable tenant context logging
azotte.setDebugMode(true);
// Log current tenant context
console.log('Current tenant:', azotte.getCurrentTenant());
// Validate tenant permissions
const permissions = await azotte.tenants.getPermissions();
console.log('Tenant permissions:', permissions);
Next Steps
- Learn about Rate Limiting
- Understand Authentication
- Explore Error Handling